Largely ripped from paper.
Definition (Byzantine Faults)
A Byzantine fault is the most severe and general type of failure in a distributed system, where a node or component can behave arbitrarily. Unlike a “fail-stop” fault (where a node simply crashes and stops responding), a Byzantine node can exhibit unpredictable and malicious behavior. For example, it might:
- Send incorrect or conflicting data to different nodes in the system.
- Fail to send messages or selectively drop messages.
- Corrupt local state or respond with garbage data.
This type of fault gets its name from the “Byzantine Generals Problem,” a logical dilemma where generals must coordinate an attack but some of them might be traitors trying to sabotage the plan by sending conflicting information.
This system has some assumptions:
- nodes are connected by a network
- a network may fail to deliver messages, delay them, duplicate them, or deliver them out of order.
- independent node failures
- each node runs different implentations of the service code and OS
- each node should have a different root password and a different admin
- cryptographic techniques to prevent spoofing and replays to detect corrupted messages
- A strong adversary
- can coordinate nodes
- delay communication (but not indefinitely)
- the nodes are computationally bound (unable to subvert cryptographic techniques above)
Definition (Safety)
The algorithm provides safety if the replicated service satisfies linearizability: it behaves like a centralized implentation that executes operations atomically one at a time. Safety requires the bound on the number of faulty replicas because a faulty replica can behave arbitrarily (can destroy its state).
Safety is also provided regardless of how many faulty clients are using the service (even with collusion). Faulty clients cannot break invariants on the service state if the service operations are designed to preserve invariants.
Safety is not sufficient for guarding against faulty clients (imagine a faulty client that writes garbage data to a NFS). Access control limits the damage that a faulty client can do.
Definition (Liveness)
The algorithm provides liveness if it guarantees that clients eventually receive replies to their requests, provided that at most replicas are faulty.
Byzantine Liveness
Because of the FLP impossibility result, PBFT cannot guarantee liveness in a purely asynchronous system. Instead, it relies on a “partial synchrony” assumption: it ensures liveness assuming that message delays do not grow faster than time indefinitely (i.e., the network eventually delivers messages within some finite, but perhaps unknown, bound).
PBFT guarantees liveness as long as at most
replicas are faulty and does not grow faster than indefinitely. Here, is the time between the moment when a message is sent for the first time and the moment when it is received by its destination (assuming the sender keeps retransmitting the message until it is received).
Lemma (Minimum Number of Replicas)
To tolerate Byzantine faults, a distributed system must have at least replicas.
Suppose we have replicas. Since we tolerate up to faults, at most the remaining replicas must be able to advance.
The worst case scenario for responses is that the nodes that failed to respond were actually honest nodes that were simply delayed by some network latency. That means that the pool of responses we did collect might include all of the malicious, lying nodes. Thus, must be from honest nodes. Therefore
since the honest nodes must outnumber the malicious nodes. So,
requiring nodes.
Privacy
The algorithm does not address fault-tolerant privacy. A faulty replica can leak private information to the adversary.
PBFT (Practical Byzantine Fault Tolerance)
The algorithm is a form of replicated state machine (RSM). The set of replicas is and each replica is denoted by an integer from . Assume where is the maximum number of faulty replicas.
Replicas move through a succession of configurations called views. In a view, one replica is the primary and the others are backups. Views are numbered consecutively. The primary of a view is the replica such that where is the view number.
The algorithm works roughly as follows:
- A client sends a request to invoke a service operation to the primary.
- The primary multicasts the request to the backups.
- The replicas execute the request and send a reply to the client.
- The client waits for replies from different replicas with the same result. This is the result of the operation.
Like all RSMs, replicas must be deterministic and they must execute from the same state. Given these two properties, the algorithm ensures the safety property by guaranteeing all non-faulty replicas agree on a total order for the execution of requests despite failures.
Each node (including active clients) share a 16-byte secret session key with each replica.
Client Operations
The client sends
where
- is the operation to be executed
- is a timestamp
- used for “exactly-once” semantics (prevent replay)
- is the client ID
- is the client’s signature on the message
Each message sent by the replicas to the client includes the current view number , allowing the client to track the view (and hence the current primary).
A client sends a request to “what it believes is the primary”, which atomically multicasts the requests to all the backups. The replica will send a reply to the client of the form
where
- is the view number
- is the timestamp from the client’s request
- is the client ID
- is the replica ID
- is the result of executing the operation
The client waits for replies from different replicas with the same timestamp and the same result before accepting as the result of the operation (recall at most replicas can be faulty).
Error Case: What if the client does not receive the replies soon enough? It will broadcast the request to all replicas.
- If it has already been processed, the replica will simply resend the reply.
- Otherwise, if the replica is not the primary, it relays the request to the primary. If the primary does not multicast the request to the group, it will be suspected as faulty, and a view change will occur.
The state of each replica includes
- state of service
- message log containing messages the replica has accepted
- integer denoting replica’s current view
When the primary , receives a client request, , it starts a three-phase protocol to atomically multicast the request to the replicas. It starts this protocol immediately unless the number of messages for which the protocol is in progress exceeds some threshold (to prevent the primary from being overwhelmed by requests).
The three phases are
- pre-prepare: the primary multicasts a pre-prepare message to the backups
- prepare: the replicas multicast a prepare message to each other
- commit: the replicas multicast a commit message to each other
Pre-Prepare Phase
The pre-prepare and prepare phase are almost exactly like Paxos Algorithm and how they choose a value. The difference is that pre-prepare
where
- is the view number
- is a sequence number (used to order requests)
- is the digest of the client request (cryptographic hash of the request, aimed to reduce the size of messages sent between replicas)
- is the message.
Since the primary itself can be malicious, the replica cannot blindly trust this message. It must independently verify:
- is the correct view number
- where are the low and high watermarks for sequence numbers ^7fbed1
- a malicious primary could try to exhaust the sequence numbers by using very large values, preventing the replicas from accepting any more requests.
- it can also reuse old sequence numbers, causing the replicas to accept old requests as new ones.
- is the digest of the client request, using Message Authentication Codes (MACs)
- Symmetric cryptography: Sender and receiver share some secret to sign their messages.
- faster than signing with cryptographic key (like RSA)
- verifies
Prepare Phase
If backup accepts , it enters the prepare phase by multicasting a
message to all other replicas and adds both messages to its log, where
- is the view number
- is the sequence number
- is the digest of the client request
- is the replica ID
A replica (including the primary) accepts prepare messages and adds them to its log provided
- the signatures are correct (was not forged)
- view numbers equal the replica’s current view
- sequence number is between . The time should be somewhat recent.
Let the predicate resolve to a Boolean value. It is true iff replica has inserted in its log
- the request
- a
pre-preparemessage for with view number and sequence number -
preparemessages from different backups that thepre-preparemessage.
The is very important. From Lemma (Minimum Number of Replicas), we ensure that we have (including itself) Quorum that ensures that if one non-faulty replica achieves a true , it is impossible for any other non-faulty replica to achieve a true for (i.e., the replicas cannot be split on different values for the same sequence number) for a different request.
Both phases guarantee that non-faulty replicas agree on a total order for the requests within a view.
Commit Phase
Then, replica enters the commit phase by multicasting a
message to all other replicas when is true. Replicas accept commit messages and add them to their logs provided they are properly signed, the view numbers match, and the sequence number is between .
We define two more predicates:
- . This is true iff is true for all in some set of non-faulty replicas.
- think of this as a “global truth”
- Although this may be true, since the network is asynchronous, an individual node cannot know about it unless it has proof.
- . This is true iff is true and replica has accepted commits (possibly including its own) from different replicas that match the pre-prepare for .
- think of this as a “local truth”
- This is the “local proof”.
- When this local predicate is true, the replica is finally authorized to execute the requested operation.
In particular,
This ensures non-faulty replicas agree on the sequence numbers of requests that commit locally even if commit in different views at each replica.
Here, is primary, are honest replicas, and is a faulty replica. The important distinction here (and why it’s different from Normal Operation Flow) is that the replicas execute the operation, not the primary (alleviates malicious primary).
Problem: Faulty Primary
This is easy. More generally, we can get a new primary. In HarpFS, this is done via view change. However, running an election may not be sufficient, as the old leader can win. PBFT fixes this by giving a deterministic primary selection algorithm. This is the , ensuring that the primary changes in a round-robin fashion.
You cannot continually rig the elections. In the worst case, we need view changes in a row.
Problem: Honest Primary + Skeptical Replicas
This is hard. How does the primary convince the replicas it is honest? We get a quorum of replicas to agree.
Garbage Collection
The logs of the replicas can grow indefinitely. To prevent this, the replicas periodically checkpoint their state and discard old log entries. We can discard requests after have been executed by at least non-faulty replicas and it can prove this to others in view changes.
If some replica misses messages that were discarded by all non-faulty replicas, it will need to be brought up to date by transferring all or a portion of the service state. Therefore, replicas also need some proof that the state is correct.
Proofs are expensive. To reduce the cost, replicas can produce a proof of their state when a sequence number is divisible by some constant (e.g. ) is executed. Let
- a checkpoint be the state produced by this request and
- a stable checkpoint be a checkpoint with a proof.
A replica maintains several logical copies of the service state:
- the last stable checkpoint
- zero or more checkpoints (that are not stable)
- a current state
Proof of Correctness
When replica produces a checkpoint, it multicasts a message
to the other replicas, where
- is the sequence number of the last request whose execution is reflected in the state
- is the digest of the state
Each replica collects checkpoint messages in its log until it has of them for sequence number with the same digest signed by different replicas (possibly including itself). These messages are proof.
A checkpoint with proof is a stable checkpoint and discards all Pre-Prepare Phase, Prepare Phase, and Commit Phase messages with sequence number from its log. We use this to advance the low and high watermarks, where
- is the sequence number of the last stable checkpoint
- where is big enough so that replicas do not stall waiting for a checkpoint.
- If we use as a checkpoint generator, then might be .
View Changes
The view change protocol provides liveness by allowing progress when the primary fails. A view change is triggered by timeouts that prevent backups from waiting indefinitely for requests to execute. A backup starts a timer when it receives a request and the timer is not already running.
If the timer of backup expires in view , the backup starts a view change to move to the system to view . It stops accepting messages (other than checkpoint, , and messages) and multicasts a
message to all replicas, where
- is the new view number
- is the sequence number of the last stable checkpoint known to
- is a set of
checkpointmessages for the stable checkpoint with sequence number (proof of ) - is a set contain a set for each request that prepared at with a sequence number higher than .
- Each contains a valid
pre-preparemessage (without the corresponding client message) and matching, validpreparemessages signed by different backups with the same view, sequence number, and digest of .
- Each contains a valid
When the primary of view receives valid view-change messages for view from different backups, it starts the view change by multicasting a
message to all other replicas, where
- is the new view number
- is the set of valid
view-changemessages that the primary received - is a set of
pre-preparemessages. It is computed as follows:- Think of it as the “carry-over” to-do list for the new primary. When the old primary failed, the network was likely in the middle of processing several client requests (local commits, still in prepare phase, some sequence slots might be empty). The new primary cannot ignore these in-flight requests otherwise it may lost data and diverge.
- The primary determines the sequence number of the latest stable checkpoint in and the highest sequence number in a
preparemessage in .- Set up the boundaries for the sequence numbers that the new primary needs to worry about.
- The primary creates a new
pre-preparemessage for view for each sequence number between and . There are two cases:- There is at least one set in the componentof some
view-changemessage in with sequence number .
- The primary creates a new message:
pre-preparemessage for sequence number with the highest view number in . 2. There is no such set.- It creates a new message with a null request:
no-opin Paxos Algorithm. - There is at least one set in the componentof some
The primary then appends the messages in to its log. If is greater than the sequence number of its own latest stable checkpoint, the primary also inserts the proof stability for the checkpoint with sequence number in its log. Then the primary enters view and can accept messages for view .
A backup accepts a new-view message for view if
- it is properly signed
- the view change messages it contains are valid for view
- if the set is correct
- It verifies the correctness by performing a similar computation to the used by the primary to create .
Then it adds the new information to its log as described for the primary, multicasts a prepare for each message in , and enters view .
Cryptography
To avoid the main performance bottleneck found in previous Byzantine-fault-tolerant systems, PBFT relies on public-key digital signatures (like RSA) sparingly. Digital signatures are only used for view-change and new-view messages, which are sent rarely.
For all other messages during normal operation (e.g., client requests, pre-prepare, prepare, commit), the algorithm uses Message Authentication Codes (MACs). MACs can be computed three orders of magnitude faster than digital signatures.
Message Authentication Codes (MACs)
Each node (including active clients) shares a 16-byte secret session key with each replica. A MAC is generated by applying MD5 to the concatenation of the message and the secret key (a variant of the secret suffix method). The system truncates the resulting MD5 digest to the 10 least significant bytes, which reduces the size of the MAC and improves resilience to certain attacks.
However, MACs have a fundamental limitation relative to digital signatures: they cannot be used to prove that a message is authentic to a third party. The algorithm circumvents this by taking advantage of specific invariants and local state.
Authenticators
The digital signature in a reply message is replaced by a single MAC because it has a single intended recipient (the client).
However, messages that are broadcast to all replicas (such as client requests or protocol phase messages) replace the digital signature with an authenticator. An authenticator is a vector of MACs with an entry for every other replica in the system. Each entry is computed using the specific session key shared between the sender and the replica corresponding to that entry.
While the time to generate an authenticator and its size grow linearly with the number of replicas, it remains significantly faster and smaller than a 1024-bit RSA signature for typical system configurations (e.g., tolerating up to simultaneous faults).