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

Date: 2026-05-29

Time: 11:49

checkprepared — PBFT Prepared Predicate and Commit Transition

Purpose

checkprepared implements the prepared predicate from the PBFT protocol. It determines whether a request has accumulated enough evidence (a PRE-PREPARE + 2f matching PREPAREs) to transition from the prepare phase to the commit phase. When the predicate is satisfied, the node broadcasts a COMMIT message and checks whether the commit threshold is also met.

This is the gatekeeper between PBFT's second and third phases. Without it, nodes would never advance to committing — and without the quorum check, a Byzantine minority could trick honest nodes into committing inconsistent requests.

Contract

Preconditions:

Postconditions:

Invariant: A given (view, seq) pair transitions to "prepared" at most once — enforced by the early-return guard on self.prepared_requests.

Parameters

| Parameter | Type | Meaning |

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

| view | int | The current view number. Identifies which primary proposed this request. |

| seq | int | The sequence number assigned by the primary. Orders requests within a view. |

| digest | str | SHA-256 hex digest of the client request. Used to ensure all nodes agree on the same request content, not just the sequence slot. |

Edge cases: If digest doesn't match any logged PREPARE messages (e.g., a Byzantine node sent garbage), the prepare_senders set stays empty and the quorum check fails harmlessly.

Return Value

Returns list[Message] — either:

The caller (handlepreprepare or handleprepare) extends its own result list with this return value, so the messages propagate up to receivemessage and out to the cluster's message bus.

Algorithm

1. Idempotency guard — If (view, seq) is already in prepared_requests, return early. This prevents duplicate COMMIT broadcasts.

2. PRE-PREPARE check — Verify at least one PRE-PREPARE exists in the log for this slot. Without the primary's proposal, there's nothing to prepare.

3. Quorum count — Iterate all logged PREPARE messages, collect senders whose digest matches. Uses a set to deduplicate — only distinct senders count. This is critical: without dedup, a Byzantine node could spam PREPAREs to inflate the count.

4. Threshold check — Require 2f distinct matching PREPAREs. In a 3f+1 system, this means the primary (who sent PRE-PREPARE, not PREPARE) plus 2f preparers form a quorum of 2f+1 nodes that agree on the request. Note: the primary is excluded from the PREPARE count by convention — handleprepare rejects PREPAREs from the primary, and the primary's handlepre_prepare path skips sending a PREPARE.

5. Mark prepared — Add (view, seq) to prepared_requests.

6. Create and log COMMIT — Build a COMMIT message from this node, append it to the local log, and register the node in phase_senders so future self-COMMITs are rejected as duplicates.

7. Chain to commit check — Call checkcommitted to see if the commit threshold (2f+1 COMMITs) is already met. This handles the case where COMMIT messages from other nodes arrived before this node reached prepared.

Side Effects

| Mutation | What changes |

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

| self.prepared_requests | (view, seq) added — marks the slot as prepared |

| self.message_log | Own COMMIT appended to the commit log for this slot |

| self.phasesenders | Own nodeid added to the COMMIT sender set — prevents self-dedup issues |

| Transitive via checkcommitted | May add to committedrequests, executedlog, and advance nextexecute_seq |

No I/O or exceptions. All mutations are in-memory.

Error Handling

None — the method uses early returns for all failure conditions. No exceptions are raised or caught. Invalid states (wrong digest, missing messages) result in a silent [] return. This is deliberate: in a Byzantine protocol, you don't tell potentially-faulty peers why you rejected their input.

Usage Patterns

Called from exactly two places:

1. handlepre_prepare — After a backup logs the PRE-PREPARE and its own PREPARE, or after the primary logs its own PRE-PREPARE (primary skips PREPARE).

2. handleprepare — After logging each incoming PREPARE message. Since each new PREPARE might push the count past 2f, every arrival triggers a re-check.

Both callers pass the digest they've already validated, so checkprepared trusts it. The callers append the returned messages to their own result lists.

Dependencies

Subtle Design Choices

The threshold is 2f, not 2f+1. This is because the primary's PRE-PREPARE counts as implicit agreement — the primary proposed this request, so it doesn't need to send a redundant PREPARE. The total agreement is PRE-PREPARE(1) + PREPAREs(2f) = 2f+1, which is the standard PBFT quorum. The code in handleprepare enforces this asymmetry by rejecting PREPAREs from the primary.

The method logs its own COMMIT before broadcasting, and manually registers in phasesenders. This is necessary because when the COMMIT eventually arrives back at this node (via the cluster bus), is_duplicate will correctly reject it as already seen.

Beliefs