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

Date: 2026-05-29

Time: 11:50

handleview_change — PBFT View Change Handler

Purpose

This method handles incoming VIEW_CHANGE messages, which are the mechanism PBFT uses to replace a faulty or unresponsive primary. When enough nodes agree that the current primary is broken, the system transitions to a new view with a new primary — without losing any requests that were already prepared. This is the liveness guarantee of PBFT: even if the primary fails, the system makes progress.

Contract

Preconditions:

Postconditions:

Invariant: self.current_view is monotonically non-decreasing.

Parameters

| Parameter | Type | Description |

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

| msg | Message | A VIEW_CHANGE message from another node. msg.view is the *proposed* new view number, not the sender's current view. msg.data["prepared"] carries the sender's prepared-but-uncommitted requests so they can survive the transition. |

Return Value

Returns list[Message] — messages to broadcast. Three possible outcomes:

1. Empty list — the message was stale, duplicate, or we're waiting for more votes

2. Single VIEW_CHANGE — this node is not the new primary but joins the vote

3. NEWVIEW + PREPREPARE messages — this node is the new primary and is taking over, re-proposing any in-flight requests

Algorithm

Step 1 — Staleness check. If msg.view <= self.current_view, the view change is for a view we've already moved past. Drop it.

Step 2 — Deduplication. Store the message in viewchangemsgs[target_view], but only if we haven't already recorded a message from this sender for that view. This prevents a Byzantine node from stuffing the ballot box.

Step 3 — Branch on role. The new primary for a view is determined by targetview % totalnodes. The method diverges:

Path A — Not the new primary:

Check whether we've already sent our own VIEW_CHANGE for this view. If not, create one (bundling our own prepared-but-uncommitted data) and broadcast it. This is the "protocol contagion" behavior — receiving a view-change message causes non-primary nodes to join the vote, which is how the quorum builds. If we already sent ours, do nothing.

Path B — We are the new primary:

Wait until we have at least 2f+1 view-change messages (the BFT quorum). Once the threshold is met:

1. Advance the view — set self.currentview = targetview, reset the timer

2. Merge prepared sets — union all prepared data from every view-change message, deduplicating by sequence number (first writer wins)

3. Broadcast NEW_VIEW — tells all nodes the view change succeeded

4. Re-propose requests — for each prepared-but-uncommitted request from the old view, issue a fresh PRE_PREPARE in the new view. This ensures no committed-by-some request is lost during the transition. The sequence counter is bumped if needed to avoid collisions.

Side Effects

Error Handling

None. The method uses silent drops (return []) for all invalid conditions: stale views, duplicates, insufficient quorum. No exceptions are raised. This is typical for Byzantine protocol implementations — you can't trust the input, so you just ignore what doesn't check out.

Usage Patterns

Called exclusively from receivemessage when msg.msgtype == MessageType.VIEW_CHANGE. The typical flow:

1. A non-primary node's timer fires → tick() calls initiateviewchange() → broadcasts a VIEWCHANGE

2. Other nodes receive it → handleviewchange fires → they echo their own VIEWCHANGE (contagion)

3. The designated new primary collects 2f+1 of these → sends NEW_VIEW + re-proposed requests

4. Other nodes process NEWVIEW via handlenewview → adopt the new view

At the cluster level, triggerviewchange() forces this by ticking all non-primary nodes past the timeout.

Dependencies

Assumptions Not Enforced by Types

1. msg.data["prepared"] structure — the code assumes each entry has sequence, request, digest, and view keys. A malformed message would raise KeyError.

2. First-writer-wins deduplication on sequence — when merging prepared sets from multiple VC messages, if two nodes prepared different requests at the same sequence number, only the first one seen survives. The PBFT safety proof guarantees at most one request can be prepared at a given (view, seq), but this code doesn't verify that.

3. Non-primary nodes don't advance their view — only the new primary sets self.currentview = targetview here. Other nodes update their view when they later receive the NEWVIEW message. Between sending their VC and receiving NEWVIEW, they're in a liminal state where they'll reject normal-phase messages for both the old and new view.

4. applybyzantine is called on outgoing but not incoming — the code trusts that Byzantine behavior is only simulated on send, not on receive. A real PBFT would need MAC/signature verification.

Beliefs