Function: applybyzantine in byzantine-fault-tolerance/pbft.py

Date: 2026-05-29

Time: 11:50

applybyzantine — Byzantine Fault Injection for PBFT Messages

Purpose

applybyzantine is a message-corruption filter that sits between a node's protocol logic and the network. Every outgoing message batch passes through it before transmission. For honest nodes it's a no-op passthrough; for Byzantine nodes it mutates, drops, or multiplies messages to simulate specific classes of faulty behavior that PBFT is designed to tolerate.

This exists to make the simulation testable — rather than writing separate "bad node" implementations, the same PBFTNode class handles all roles. The Byzantine mode is set at construction time and applied uniformly to all outbound traffic.

Contract

Parameters

| Parameter | Type | Description |

|-----------|------|-------------|

| messages | list[Message] | Outgoing messages produced by the node's protocol handlers. These are freshly constructed by the caller, so mutating them in-place is safe. |

Edge case: an empty list is valid input and returns an empty list for all modes.

Return Value

A list[Message] that the caller (receivemessage, submitrequest, tick, handleview_change) forwards to the cluster's message bus. The caller does not inspect or filter the result — it trusts this method completely, which is the point.

Algorithm

The method dispatches on self.byzantine_mode through a chain of early-return if blocks:

1. HONEST — Return messages unchanged. This is the fast path and is checked first.

2. SILENT — Return an empty list. The node produces valid internal state (it still logs messages, advances sequences) but nothing leaves. This simulates a crash fault or a node that simply stops participating.

3. WRONG_SEQUENCE — Add 1000 to every message's sequence number. Honest nodes will reject these because the sequence won't match any accepted pre-prepare. This tests that nodes validate sequence numbers rather than blindly trusting them.

4. WRONGDIGEST — Replace every message's digest with a deterministic bad value ("baddigest{nodeid}"). This tests that nodes verify digest consistency across the pre-prepare/prepare/commit chain. The node-specific string ensures different Byzantine nodes produce different bad digests.

5. EQUIVOCATING — The most sophisticated mode. For each broadcast message (recipient is None), it creates *N-1* targeted copies, one per peer, each with a different digest computed from f"equivoc{peer}{sequence}". This means every peer receives a message that appears valid but disagrees with what every other peer received. Already-targeted messages pass through unchanged. This is the classic equivocation attack: a Byzantine primary sends conflicting pre-prepares to different replicas, trying to split the quorum.

6. Fallthrough — If none of the modes match (shouldn't happen with the defined constants), return messages unchanged.

Side Effects

Error Handling

None. No exceptions are raised or caught. Invalid modes silently return the original messages. There's no logging or alerting when Byzantine behavior is applied — this is intentional for a simulation.

Usage Patterns

The method is called in exactly four places, always wrapping the output of a protocol handler:


# In receive_message:
return self._apply_byzantine(self._handle_pre_prepare(message))
return self._apply_byzantine(self._handle_prepare(message))
return self._apply_byzantine(self._handle_commit(message))

# In submit_request:
result = self._apply_byzantine([pp])

# In tick:
return self._apply_byzantine(self._initiate_view_change())

# In _handle_view_change:
return self._apply_byzantine([vc])

Note that handleviewchange also calls apply_byzantine when re-proposing prepared requests during a view change — Byzantine behavior is applied consistently to *all* outbound paths.

Caller obligation: the caller must not reuse the message objects after passing them in, since WRONGSEQUENCE and WRONGDIGEST mutate them.

Dependencies

Assumptions Not Enforced by Types

1. self.byzantine_mode is assumed to be one of the five ByzantineMode constants, but it's a plain str — any string is accepted at construction.

2. self.totalnodes and self.nodeid are assumed valid (non-negative, nodeid < totalnodes) — used in EQUIVOCATING to iterate peers.

3. Messages in the input list are assumed to be freshly created and not shared with other references — the in-place mutation of WRONGSEQUENCE/WRONGDIGEST would corrupt shared state otherwise.

4. dict(m.data) in the EQUIVOCATING branch is a shallow copy — if data contains nested mutable objects, the equivocated messages would share references with the original.