https://github.com/hackenproof-public/somnia
When the current leader of a view in PBFT does not respond within a given timeout window, the nodes advance to the next view to get a new leader and continue the consensus process. In this case, all nodes send a LeaderViewChangeMessage to the new leader so that he can use the nodes votes to move to the next view:
https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_view_manager.cc#L590-L595
The new consensus leader then builds a NewViewMessage, including all the LeaderViewChangeMessages, so that the nodes can verify that the view change has been agreed upon by a 2f+1 majority of nodes. This message is then broadcast to all nodes, allowing them to update their view:
https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_view_manager.cc#L748-L755
All nodes then attempt to switch to the view in TryEnterNewView:
https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_view_manager.cc#L765
In this function, GetLockedRequestBatchesForNewView is used to obtain the request batches that should be carried over into the next round. This is determined based on the request batches provided in the LeaderViewChangeMessages that are included in the NewViewMessage. To decide which request batches must be taken in the next view a LeaderViewChangeMessage has three fields:
latest_observed_checkpoint (https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_types.h#L472): This is the latest checkpoint of the node that sent the message. It is needed to determine the first sequence number that has not been checkpointed and will probably need to be included in the next view.certificate_rounds_above_checkpointed (https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_types.h#L477): These are the sequence numbers that already have a certificate and probably need to be committed or re-proposed in the next view.preprepared_rounds_above_checkpointed (https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_types.h#L489): These are sequence numbers with views that are currently in the pre-prepare round and are only re-proposed when they have f+1 votes.In GetLockedRequestBatchesForNewView, the latest_observed_checkpoint field is first used to determine the maximum checkpointed sequence number. At this point, the certificate is validated if necessary:
https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_view_manager.cc#L983-L986
Then certificate_rounds_above_checkpointed is used to determine the highest view that has a certificate for a given sequence number. At this stage, both the certificate and the underlying request batch are verified:
https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_view_manager.cc#L1085-L1087
As a last step, preprepared_rounds_above_checkpointed is used to determine the views in the sequence numbers that have more than f+1 votes and should be re-proposed. In this step, the data is not validated, and a malicious validator could include a request batch with arbitrary payloads because the hashes are not checked. Normally, a validator would need f+1 votes for a request batch to be re-proposed, but since the hashes are not verified, the validator could set the request batch hash to a batch that already has more than f+1 votes but with malicious request payloads:
https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_view_manager.cc#L1111-L1150
One thing an attacker must pay attention to is that their votes must be the ones crossing the f+1 threshold; otherwise, another request batch without a malicious payload will already have been included in proposal_majority_rounds_for_new_view and the attacker’s request batch will not be set:
https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_view_manager.cc#L1125-L1135
This can be done most efficiently by an attacker who waits to become the consensus leader, because then they can simply order the LeaderViewChangeMessages in NewViewMessage and set their request batch so that it is included in proposal_majority_rounds_for_new_view in GetLockedRequestBatchesForNewView.
After this, the request batch is set for the new round. An attacker could exploit this when they are the consensus leader by sending one half of the nodes a request batch and the other half a request batch with different payloads but the same hash. Since the hashes are not verified until execution, one half of the network will execute different transactions than the other half. This results in divergent blocks and causes the chain to fork. After about 16 blocks, the chain will halt because the sequence commitments included in the blocks no longer match, and no transactions can be executed anymore: https://github.com/hackenproof-public/somnia/blob/5931d305dd6fb02b637efd2fca13e77731d05804/somnia/pbft/pbft_round_manager.cc#L219-L236
The hashes of the request batch should be verified in GetLockedRequestBatchesForNewView when adding a round to proposal_majority_rounds_for_new_view
The following modifications must be applied to the code to run the POC. The reasons for some of the modifications are explained in the comments accompanying the changes: https://gist.github.com/TheSchnilch/a618c01e1b7913e3697495a57396aa13
The POC can be executed with NETWORK_PRESET=mainnet-small ./ci/run-local-deployment.sh
preprepared_rounds_above_checkpointed in the LeaderViewChangeMessage.preprepared_rounds_above_checkpointed.NewViewMessages: one containing the correct request payloads for sequence number 5, and another with no payloads.NewViewMessage, while the other half receives the second message.After this the output of the nodes should look like the one in screenshot.
To verify that the chain actually forked this curl request can be used on node http://127.0.0.1:6500 and http://127.0.0.1:6501 to see that the block hashes are different at block 400 (block 400 because we are in epoch 3 and 3*128 + 5 = 389. This means block 400 is one of the forked):
curl -X POST -H "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x190", false],"id":1}' \
http://127.0.0.1:6500