Qora-FL
Quorum-Oriented Robust Aggregation for Federated Learning
Problem
Federated learning systems are fragile under adversarial or faulty clients. Standard aggregation methods silently fail under model poisoning, gradient manipulation, or non-IID drift.
Qora-FL provides Rust-backed robust aggregation methods for federated learning. Each method has explicit assumptions and tolerance conditions, documented below. The included experiments evaluate selected attacks with up to 30% adversarial clients.
[!IMPORTANT] Qora-FL is an experimental robust-aggregation toolkit. Its algorithms are implemented and tested, but the project has not received an independent security review. Robustness depends on each method's stated assumptions, threat model, and correct configuration.
Currently implemented
Behavior available through tested public paths:
- FedAvg baseline, with optional sample-count weighting
- Coordinate-wise median
- Coordinate-wise trimmed mean
- Krum and Multi-Krum, with their preconditions enforced
- Input validation: dimensions, non-finite rejection, client-ID alignment
- Krum quorum enforcement (
n >= 2f + 3) - Multi-Krum selection-bound enforcement (
1 <= m <= n - 2f - 2) - Reputation tracking and participation gating
- Optional norm-bound filtering, opt-in and off by default
- Fail-closed behavior when filtering rejects every client
- Reputation numeric invariants (scores finite and within
[0, 1]) - Rust API and Python bindings
- Flower-compatible strategy adapter
- Rust CI across three operating systems; Python CI including the adapter
Experimental or planned
Present in the codebase but not wired into the normal aggregation path, or not yet implemented:
- Reputation-weighted robust aggregation (cubic influence weighting exists as a utility only)
- Adaptive or percentile-derived norm bounds (today the caller picks a fixed bound)
- Audit records in Python. The Rust API returns a typed, versioned record from
aggregate_with_audit; the bindings do not expose it yet, so that a binding design is not frozen while the schema is still experimental. - Cross-platform bit-identical end-to-end aggregation
- Independent security review
- Real-world deployment validation
- Additional attack and dataset evaluations
Architecture
Clients produce model updates
│
▼
┌───────────────────────────┐
│ Qora Robust Aggregator │
│ │
│ ┌─────────────────────┐ │
│ │ Trimmed Mean │ │
│ │ Median │ │
│ │ Krum (n ≥ 2f+3) │ │
│ │ FedAvg (baseline) │ │
│ └─────────────────────┘ │
└─────────────┬─────────────┘
│
Deviation signal (not trust)
│
▼
┌─────────────────┐
│ Reputation │
│ Manager │
│ │
│ Scores / Gates │
│ / Weighting │
└────────┬────────┘
│
▼
Verified Global Update
flowchart LR
C1[Client 1] --> U[Client Updates]
C2[Client 2] --> U
Cn[Client N] --> U
U --> A[Qora Aggregator]
A -->|Deviation Signal| R[Reputation Manager]
R -->|Scores / Gates| A
A --> G[Aggregated Model]
style A fill:#f3f6ff,stroke:#4a6cf7,stroke-width:2px
style R fill:#fff7e6,stroke:#f59e0b,stroke-width:2px
Determinism
Qora-FL uses integer arithmetic for Krum distance and score calculations after BFP-16 encoding (block floating-point, 16-bit mantissas, per-vector shared exponent). Identical BFP-encoded inputs therefore produce deterministic Krum scores and rankings.
Full cross-platform bit-identical aggregation is not currently guaranteed. The pipeline is only partly integer:
| Stage | Arithmetic |
|---|---|
| BFP-16 encoding | floating-point (log2, scaling, rounding) |
| Krum distances and scores | integer (i32 shifts, i64 accumulation, saturating) |
| Krum result | the selected original f32 update |
| Multi-Krum result | mean of selected original updates, in f32 |
| Median, trimmed mean, FedAvg | floating-point throughout |
So the determinism claim covers the scoring stage, not the end-to-end result.
Selection rankings are reproducible; the returned values are f32 and inherit
whatever the encoding and averaging stages do.
Reputation
Reputation is not trust. It is a deviation-derived signal measuring how far a client's update sits from the aggregate that round.
What it does today:
- Tracks scores across rounds, persisted via JSON serialization
- Gates participation: clients below the configured threshold are excluded before aggregation
- Fails closed: if every identified client is below the threshold, the round
returns
AllUpdatesRejectedrather than reinstating them - Maintains numeric invariants: scores are finite and within
[0, 1], and invalid mutation inputs are rejected rather than clamped
Norm-bound filtering does not feed reputation: a client excluded by the bound is left out of the round and its score is untouched.
What it does not do yet: cubic influence weighting (min(rep^3, 0.8)) and
its cap are available as utilities, and a caller may use those values directly.
The primary aggregation path does not currently consume them, so the cap does
not provide protection for the default aggregation workflow.
Reputation-weighted robust aggregation is roadmap work.
Details that affect configuration:
- Unknown clients start at the default score of 0.5. A ban threshold above 0.5 therefore rejects every first-round participant -- which now fails the round rather than passing it through ungated.
- Through the Flower adapter, reputation is computed from the complete flattened model, and each client receives at most one update per round. The adapter performs no gating of its own.
Quick Start (Python + Flower)
Flower is an optional extra. The core bindings work without it:
import qora, ByzantineAggregator, and ReputationManager all function with
Flower absent. Only qora.QoraStrategy requires it, and importing it without
Flower installed raises an ImportError naming the extra to install.
# Quote the extra: some shells (zsh, fish) treat brackets as globs.
pip install "qora-fl[flower]"
Supported range flwr>=1.5,<2, upper-bounded because the adapter targets
Flower's 1.x strategy interface. CI tests both ends explicitly: 1.5.0 and
1.32.1. Versions in between are covered by the declared promise but are not
individually exercised.
import flwr as fl
from qora import QoraStrategy
strategy = QoraStrategy(
aggregation_method="trimmed_mean",
trim_fraction=0.2,
min_fit_clients=5,
# norm_bound=None by default: no filtering, no norm computation.
)
fl.server.start_server(
server_address="0.0.0.0:8080",
config=fl.server.ServerConfig(num_rounds=10),
strategy=strategy,
)
QoraStrategy inherits from FedAvg, so standard Flower parameters
(fraction_fit, min_fit_clients, initial_parameters, accept_failures,
fit_metrics_aggregation_fn) are accepted. It is a compatibility adapter,
not a behavioral replacement for every Flower built-in strategy.
What the adapter does:
- One client result becomes one complete Rust update. Each client's model is flattened into a single update, aggregated in one call, then split back into the original layer shapes. Aggregating layer by layer would let a selection method pick a different client per layer and return a model no client submitted.
- Validates the complete model structure against the first client as
reference: layer count, per-layer shape, floating-point dtype, and finiteness.
A malformed successful result raises
ValueErrornaming the client and layer rather than being dropped -- discarding it would change the round's effective adversarial fraction. - Aggregates at
float32.float64input is accepted and converted; its extra precision is not preserved. Integer parameter arrays are rejected rather than silently coerced into trainable floats. - FedAvg weights by
num_examples. Robust methods (median, trimmed mean, Krum, Multi-Krum) treat each accepted client update as one participant --num_examplesis never reinterpreted as an algorithmic weight there, since an attacker claiming a large sample count would otherwise gain proportional influence over a median. - Honors
accept_failures. Reported failures refuse the round when it is False, returning(None, {})without invoking Rust, moving reputation, or calling the metrics callback. - Aggregates metrics only through
fit_metrics_aggregation_fn. With none configured it returns{}. Reputation is available throughQoraStrategy.get_reputation(client_id). - Performs no gating of its own. Reputation gating and updates happen once, in the Rust aggregator.
- Enables norm-bound filtering only when
norm_boundis passed, and applies it to the complete flattened model rather than to individual layers.
Python (Standalone)
pip install qora-fl
import numpy as np
from qora import ByzantineAggregator
agg = ByzantineAggregator("trimmed_mean", 0.3)
# 7 honest clients + 3 Byzantine attackers
updates = [np.array([[1.0, 2.0]], dtype=np.float32) for _ in range(7)]
updates += [np.array([[100.0, 200.0]], dtype=np.float32) for _ in range(3)]
result = agg.aggregate(updates)
# Result is [1.0, 2.0] -- attackers rejected
Norm-bound filtering
Opt-in, and off by default. Without norm_bound, no norm is computed and
behavior is unchanged.
from qora import ByzantineAggregator
agg = ByzantineAggregator("median", 0.0, norm_bound=10.0)
print(agg.norm_bound) # 10.0, or None when disabled
Nonedisables it. A supplied bound must be finite and strictly positive; zero, negative, NaN, and infinite bounds raiseValueErrorrather than being clamped.- The norm is computed in
f64and compared inclusively -- a norm exactly equal to the bound participates. - A rejected update takes its client ID and its FedAvg sample weight with it, so a rejected weight never reaches the numerator or the denominator.
- A rejected update does not affect reputation. A large norm is a policy violation, not proof of malicious intent; this filter does not claim to detect Byzantine behavior.
- If every update is rejected -- by reputation gating, by the bound, or by a
mixture -- the round raises
ValueErrorrather than aggregating a partial or best-effort result. - Method quorum is checked after filtering, so filtering can shrink a valid cohort into one Krum or Multi-Krum refuses. That is reported, not repaired.
Through the Flower adapter, pass norm_bound= to QoraStrategy. The bound then
applies to each client's complete flattened model rather than to individual
layers.
Audit records are Rust-only in 0.4.0
The Rust API returns a typed, versioned AggregationAuditEntry from
aggregate_with_audit, recording one decision per submitted update with typed
rejection reasons. The Python bindings do not expose it. Mirroring the schema
as a hierarchy of Python classes would freeze a binding design while the schema
is still experimental; a later release can return the serialized shape as a
dictionary instead.
What Python does expose is the configuration (norm_bound) and the behavior it
produces: an over-bound update is excluded, and a round that rejects everything
raises ValueError. The per-client reasons behind that error are not available
from Python yet.
Reputation Tracking
from qora import ReputationManager
rep = ReputationManager(ban_threshold=0.2)
rep.reward("hospital_A", 0.02)
rep.penalize("hospital_bad", 0.08)
print(rep.is_banned("hospital_bad")) # True after enough penalties
print(rep.active_clients()) # Only non-banned clients
# Persist between server restarts
json_state = rep.to_json()
rep2 = ReputationManager.from_json(json_state, ban_threshold=0.2)
Rust
cargo add qora-fl
use qora_fl::{ByzantineAggregator, AggregationMethod};
use ndarray::array;
let mut agg = ByzantineAggregator::new(AggregationMethod::TrimmedMean, 0.3);
let updates = vec![
array![[1.0, 2.0]], // Honest
array![[1.1, 2.1]], // Honest
array![[0.9, 1.9]], // Honest
array![[100.0, 200.0]], // Byzantine attacker
];
let result = agg.aggregate(&updates, None).unwrap();
// Result is close to [1.0, 2.0], attacker ignored
Individual Functions
use qora_fl::{trimmed_mean, median, fedavg};
use ndarray::array;
let updates = vec![array![[1.0]], array![[2.0]], array![[3.0]]];
let tm = trimmed_mean(&updates, 0.2).unwrap();
let med = median(&updates).unwrap();
let avg = fedavg(&updates, None).unwrap();
Aggregation methods and their assumptions
n is the number of accepted client updates; f is the configured Byzantine
bound; m is the number of Multi-Krum candidates selected.
| Method | Enforced precondition | Assumption for robustness |
|---|---|---|
TrimmedMean |
0.0 <= trim_fraction <= 0.5, and at least one value must survive trimming |
Depends on trim fraction, adversarial proportion, attack model, and the distribution of honest updates |
Median |
none beyond shared input validation | Strictly fewer than 50% adversarial values per coordinate |
Krum |
n >= 2f + 3 |
Blanchard et al. (2017) |
MultiKrum |
n >= 2f + 3 and 1 <= m <= n - 2f - 2 |
Blanchard et al. (2017) |
FedAvg |
weights, when supplied, must be finite and non-negative with a positive total | None. Conventional sample-weighted baseline; provides no Byzantine robustness by itself |
Trimmed mean removes the configured fraction of the smallest and largest
values independently in each coordinate. A trim fraction is not an attacker
percentage: trim_fraction = 0.2 does not establish tolerance of 30% malicious
clients, and no such universal guarantee is claimed here.
Median requires an honest majority per coordinate. Exactly 50% adversarial is not strictly fewer than half, so "tolerates 50%" would be wrong.
Krum rejects configurations violating n >= 2f + 3 with
InsufficientQuorum. There is no best-effort path below quorum.
Multi-Krum rejects an explicit m outside 1..=n - 2f - 2 with
InvalidMultiKrumSelection. When m is omitted, Qora-FL uses
min(3, n - 2f - 2); explicit unsafe values are rejected rather than silently
reduced.
Experiments
Qora-FL includes an attack-evaluation script for comparing aggregation methods across selected attacks and adversarial-client fractions. The repository does not currently publish verified benchmark results from a pinned dataset artifact and repeated multi-seed evaluation. Run the included experiment to evaluate the methods in your own environment.
See the root README for the full configuration and its limitations.
# Aggregation overhead (Python)
python examples/benchmark_overhead.py
# MNIST poisoning: FedAvg vs Qora-FL under 30% attack
pip install matplotlib scikit-learn
python examples/mnist_poisoning_demo.py
# Criterion benchmarks (Rust)
cargo bench
# Rust examples
cargo run --example quickstart
cargo run --example compare_methods
Validation status
Qora-FL is currently validated through:
- the Rust unit and integration test suite
- Python binding and Flower adapter tests
- cross-platform Rust CI (Linux, macOS, Windows) and Python CI
- Flower compatibility tests at both ends of the supported version range
- algorithm preconditions enforced in code and covered by tests
- the included examples and benchmarks, under their documented configurations
This is engineering validation. It is not an independent security review, and it does not establish robustness outside each algorithm's stated assumptions. See SECURITY.md for the boundaries.
Background
The aggregation algorithms in Qora-FL (trimmed mean, coordinate-wise median, Krum, Multi-Krum) are standard Byzantine-robust methods from the federated learning literature [1][2].
Project history
Qora-FL grew out of aggregation and distributed-trust experiments explored in the earlier QRES project (CavinKrenik/QRES_RaaS, historical work). Design choices here -- deviation-derived reputation, the BFP-16 scoring path, adaptive trim fraction, and the influence cap -- were informed by those earlier experiments and long-horizon simulations.
Qora-FL is a separate implementation with its own tests, experiments, and assumptions. Results or guarantees from QRES do not automatically apply to Qora-FL, and none of the evidence in this repository is inherited from it.
Design Philosophy
Qora-FL treats aggregation as a decision process, not a statistical convenience. The system favors explicit constraints, observable signals, and predictable failure modes over opaque optimization.
Roadmap
- Weighted robust aggregation (reputation-scaled trimmed mean)
- Adaptive trim selection based on detected attack intensity
- TensorFlow Federated adapter
- Formal verification experiments for the integer scoring path
License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qora_fl-0.4.0.tar.gz.
File metadata
- Download URL: qora_fl-0.4.0.tar.gz
- Upload date:
- Size: 195.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
203b9d378d6013afd087c916c4cc17b95e33f78ce59a83f5a395bca0af24ee23
|
|
| MD5 |
3bcf58b8832602878bde436c1f0036eb
|
|
| BLAKE2b-256 |
529a73d5d5bb4557ad7763a380094f3a4fcda045a7c46820d78bbbda7fa3797e
|
Provenance
The following attestation bundles were made for qora_fl-0.4.0.tar.gz:
Publisher:
release.yml on CavinKrenik/qora-fl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qora_fl-0.4.0.tar.gz -
Subject digest:
203b9d378d6013afd087c916c4cc17b95e33f78ce59a83f5a395bca0af24ee23 - Sigstore transparency entry: 2328504834
- Sigstore integration time:
-
Permalink:
CavinKrenik/qora-fl@8501fd6b351a258ec638859b11062c259ccc6168 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/CavinKrenik
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8501fd6b351a258ec638859b11062c259ccc6168 -
Trigger Event:
release
-
Statement type:
File details
Details for the file qora_fl-0.4.0-cp38-abi3-win_amd64.whl.
File metadata
- Download URL: qora_fl-0.4.0-cp38-abi3-win_amd64.whl
- Upload date:
- Size: 342.5 kB
- Tags: CPython 3.8+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
366326eda7a655a37e8e4ea97f1867fb75434d67534ab467f7f4c5c7c41138a7
|
|
| MD5 |
b68082a1ecec924d285566c6a1d0eb2d
|
|
| BLAKE2b-256 |
f194cb76c7aba6658d5a7f290e35542e5a2b490ba49c099caec88ad9d837bc46
|
Provenance
The following attestation bundles were made for qora_fl-0.4.0-cp38-abi3-win_amd64.whl:
Publisher:
release.yml on CavinKrenik/qora-fl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qora_fl-0.4.0-cp38-abi3-win_amd64.whl -
Subject digest:
366326eda7a655a37e8e4ea97f1867fb75434d67534ab467f7f4c5c7c41138a7 - Sigstore transparency entry: 2328506337
- Sigstore integration time:
-
Permalink:
CavinKrenik/qora-fl@8501fd6b351a258ec638859b11062c259ccc6168 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/CavinKrenik
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8501fd6b351a258ec638859b11062c259ccc6168 -
Trigger Event:
release
-
Statement type:
File details
Details for the file qora_fl-0.4.0-cp38-abi3-manylinux_2_34_x86_64.whl.
File metadata
- Download URL: qora_fl-0.4.0-cp38-abi3-manylinux_2_34_x86_64.whl
- Upload date:
- Size: 525.1 kB
- Tags: CPython 3.8+, manylinux: glibc 2.34+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f53829105227a23349f0888188f7cf3bb64c98256fa42629a6c9bcde42e03b7c
|
|
| MD5 |
5858c27dbbdb3b45d324c612f69071f4
|
|
| BLAKE2b-256 |
4b2f45a9dc4bbc85af2fced767ba2eae30131b79b4c3a2e93df52d7e65cecaea
|
Provenance
The following attestation bundles were made for qora_fl-0.4.0-cp38-abi3-manylinux_2_34_x86_64.whl:
Publisher:
release.yml on CavinKrenik/qora-fl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qora_fl-0.4.0-cp38-abi3-manylinux_2_34_x86_64.whl -
Subject digest:
f53829105227a23349f0888188f7cf3bb64c98256fa42629a6c9bcde42e03b7c - Sigstore transparency entry: 2328505923
- Sigstore integration time:
-
Permalink:
CavinKrenik/qora-fl@8501fd6b351a258ec638859b11062c259ccc6168 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/CavinKrenik
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8501fd6b351a258ec638859b11062c259ccc6168 -
Trigger Event:
release
-
Statement type:
File details
Details for the file qora_fl-0.4.0-cp38-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: qora_fl-0.4.0-cp38-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 453.9 kB
- Tags: CPython 3.8+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8215390b81cc18cc9942490ae7373737bb880557151ffd99c833a14e19ac2971
|
|
| MD5 |
4824af9fe757abfa24e1617cc5d13f95
|
|
| BLAKE2b-256 |
2dfefa8f9a0500f89b94b2ed90defec2c5a25a2d64f5b7bfb99d221f7103b997
|
Provenance
The following attestation bundles were made for qora_fl-0.4.0-cp38-abi3-macosx_11_0_arm64.whl:
Publisher:
release.yml on CavinKrenik/qora-fl
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qora_fl-0.4.0-cp38-abi3-macosx_11_0_arm64.whl -
Subject digest:
8215390b81cc18cc9942490ae7373737bb880557151ffd99c833a14e19ac2971 - Sigstore transparency entry: 2328505484
- Sigstore integration time:
-
Permalink:
CavinKrenik/qora-fl@8501fd6b351a258ec638859b11062c259ccc6168 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/CavinKrenik
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8501fd6b351a258ec638859b11062c259ccc6168 -
Trigger Event:
release
-
Statement type: