Skip to content

Attacks

Velocity-FL splits the attack surface into two honest families that operate at different layers of the stack:

  • Round-level attacks live in the Rust core and run during the orchestrator's round — they corrupt weights or inject sybils after a client has trained but before (or while) aggregation happens.
  • Data-pipeline attacks live on the Python side and run inside the client's own data loader — they corrupt labels or features before any training happens, simulating an adversary with control over a participant's data at rest.

The split is deliberate: the Rust core never sees raw labels or input features, so it can't honestly implement label-flipping; equally, the Python data layer can't reach into the round's client roster, so it can't honestly implement sybil injection. Each attack lives where its semantics actually fit.

Round-level attacks (velocity.attacks)

Implemented as crate::security::AttackType variants in vfl-core/src/security.rs. Registered via server.simulate_attack(...).

Attack Parameter What it does
model_poisoning intensity ∈ [0, 1] Sign-flips a fraction of one client's weights, scaled by intensity. Directly attacks the aggregator.
sybil_nodes count ≥ 1 Injects count synthetic clients with random gradients into the round. Amplifies the malicious vote share.
gaussian_noise intensity ≥ 0 (σ) Adds N(0, σ²) noise to the aggregated global weights. Simulates unreliable channels or gradient leakage.

Register via Python

from velocity import VelocityServer, FedMedian

server = VelocityServer(
    model_id="demo/model",
    dataset="demo/dataset",
    strategy=FedMedian(),  # robust strategy — pairs well with attacks
)

server.simulate_attack("model_poisoning", intensity=0.3)
server.simulate_attack("sybil_nodes", count=5)

summaries = server.run(min_clients=10, rounds=5)
for s in summaries:
    for result in s["attack_results"]:
        print(result)

Multiple attacks can be registered before a round — they are all applied.

Register via CLI

uv run velocity simulate-attack model_poisoning --intensity 0.2
uv run velocity simulate-attack sybil_nodes --count 5
uv run velocity simulate-attack gaussian_noise --intensity 0.1

Each invocation registers one attack and runs one round, emitting a single JSON summary on stdout.

Reading the results

Each round summary contains an attack_results list. In Python you can hydrate each entry into an AttackResult dataclass:

from velocity.attacks import AttackResult

for s in summaries:
    for raw in s["attack_results"]:
        result = AttackResult.from_dict(raw)
        print(result)
# [model_poisoning] Poisoned 2 updates at intensity=0.30 (severity=0.412, clients=2)
AttackResult field Type Description
attack_type str Which attack produced this result.
clients_affected int Number of clients touched this round.
severity float Aggregator-relative impact score in [0, 1].
description str Human-readable summary.

Data-pipeline attacks (velocity.data_attacks)

Implemented as pure-PyTorch tensor transforms in python/velocity/data_attacks.py. Compose with local_train(label_attack=…) to corrupt the labels seen by a specific client during training.

Attack Parameters What it does
apply_label_flipping num_classes ≥ 2, seed Bijective derangement of the label space — every class maps to a different class. Untargeted "generic damage" primitive (Biggio et al., ICML 2012).
apply_targeted_label_flipping source_class, target_class, flip_ratio ∈ [0, 1] Flips a fraction of source_class labels to target_class. Targeted misclassification primitive (Tolpegin et al., ESORICS 2020).

Compromising a client

from velocity.data_attacks import make_label_flip_callback
from velocity.training import local_train

# Bijective flipping for compromised clients 0 and 3
flip_cb = make_label_flip_callback(num_classes=10, seed=42)

for i, client in enumerate(split.clients):
    label_attack = flip_cb if i in {0, 3} else None
    local_train(
        local_model,
        client.loader,
        epochs=1,
        lr=0.01,
        label_attack=label_attack,
    )

The callback is applied inside the local training loop on every minibatch of the affected client — simulating a worker whose dataset has been mislabeled at rest. Honest clients see clean labels.

For a targeted flip:

flip_cb = make_label_flip_callback(
    num_classes=10,
    targeted=True,
    source_class=9,
    target_class=1,
    flip_ratio=1.0,  # all 9s become 1s
)

Demo: label-flipping vs robust aggregation

examples/mnist_label_flipping_vs_robust.py runs FedAvg vs Multi-Krum under a 20% label-flipping attack and asserts the gap — the convergence test that catches a regression in either the data-attack pipeline or the robust aggregator.

Adding a new attack

Round-level: kernels live in vfl-core/src/security.rs. Implement the mutation in Rust, expose it through the orchestrator's register_attack dispatch (vfl-core/src/lib.rs), then add the identifier to VALID_ATTACKS in python/velocity/attacks.py.

Data-pipeline: implement the transform in python/velocity/data_attacks.py as a pure tensor → tensor function plus a closure factory that pre-computes any randomness once. Then add it to DATA_ATTACK_TYPES and document the parameter contract here.