Introduction
In April 2023, a malicious proposer tricked Ultrasound’s relay into revealing the contents of a block while committing to a conflicting, invalid block. The attacker performed an unbundling attack, reordering transactions against the builder’s will and sandwiching traders, that profited between $20 and $30 million. On November 23, 2024, we responsibly disclosed a variant of this attack on Titan Relay introduced by support for the Deneb hard fork.
To support the Deneb hard fork, Flashbots upgraded its relay implementation and added a check on the integrity of the KZG commitments, a cryptographic scheme to commit to a value that can later be revealed, within the proposer’s response. Titan Relay’s alternative relay implementation, Helix, missed this key validation, which would have allowed trusted actors to attempt unbundling without the risk of slashing.
Relays implemented three safeguards following the 2023 incident:
- validate the integrity of the signed header,
- require local beacon nodes to verify the signed beacon block before broadcasting it,
- wait 1 second before sending the unblinded block back to a propose.
Because of the local verification done before the broadcast performed by Lighthouse’s implementation of the publishBlockV2
Beacon API, Titan’s production environment would have prevented untrusted proposers from receiving the block if they attempted the attack that follows.
Proposer-builder Separation
Proposer-builder separation allows beacon chain validators to outsource their block building to specialized entities, builders, who provide the validator a block via a trusted intermediary, relays, when it is the validator’s turn to propose i.e. they are a proposer. Builders trust the relay to keep the contents of blocks confidential until the proposer has committed to proposing the block by signing its header.
If the proposer can leak the contents of the block prematurely, they may maliciously alter the order of transactions the builder provided. For instance, builders may offer services to traders that wish to keep their orders private so it won’t be front-run and affect the price at which the trader purchases a given asset.
Building an alternate block is latency-sensitive as proposers have a strict window of time to gossip and receive sufficient attestations. Relays have attempted to deter proposers from risking their validator rewards by making it likely they will miss their slot if they do not immediately propose the builder’s original blocks due to the relay’s delayed response. Further, relays ensure proposers will get slashed for proposing two blocks at the same height by broadcasting the block the proposer initially committed to.
Builder, Beacon, and Blob
During a proposer's slot, it may request a payload from a relay. It will sign and thereby commit to proposing this block without seeing the entire payload, e.g., its transactions, which is why it is referred to as “blinded”. Then, the proposer will send the signed, blinded beacon block back to the relay and receive an “unblinded” block”, which reveals its transaction contents, in return.
After the April 2023 incident, relays introduced a delay in their response to the proposer, during which they will gossip the block and blob sidecars, if any, to the beacon chain, giving the block time to disseminate and reduce the likelihood that a proposer can build and propose a conflicting block. However, this delay only gives the relay an advantage if the block and blob sidecars are valid because peers will reject and not process invalid proposals.
When a beacon node receives a blob sidecar, it will reject it if the KZG commitments, which are polynomial commitments to blobs of data, do not correspond to the sidecar’s blob contents, as stated in the specification:
[REJECT] The sidecar's blob is valid as verified by verify_blob_kzg_proof(blob_sidecar.blob, blob_sidecar.kzg_commitment, blob_sidecar.kzg_proof).
Additionally, when a beacon node receives a block, it will check that it has seen or can request associated blobs. If a blob sidecar is rejected, the blobs will not be available. Thus, being able to send invalid commitments from the relay defeats any advantage gained by propagating it as beacon nodes will reject the block for containing unavailable data.
# [New in Deneb:EIP4844] # Check if blob data is available assert is_data_available(hash_tree_root(block), block.body.blob_kzg_commitments)
Corrupt Commitments
The validations performed on the KZG commitments in Helix only require an attacker to ensure that the length of blobs and commitments must be the same in their submitBlindedBlock
request. Instead of sending the correctly signed beacon block back to the relay, a malicious proposer could manipulate the commitments and attempt to build a more profitable block from the transactions the relay was tricked into providing.
While the relay's validate_header_equality
method verifies that the contents of the signed execution payload header are equivalent to what the builder supplied, it does not bind the proposer to the KZG commitments (notice their absence in the Deneb fields below). Thus, invalid KZG commitments can be provided and still pass signature verification and header validation.
class ExecutionPayloadHeader(Container):
# Execution block header fields
parent_hash: Hash32
fee_recipient: ExecutionAddress
state_root: Bytes32
receipts_root: Bytes32
logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM]
prev_randao: Bytes32
block_number: uint64
gas_limit: uint64
gas_used: uint64
timestamp: uint64
extra_data: ByteList[MAX_EXTRA_DATA_BYTES]
base_fee_per_gas: uint256
# Extra payload fields
block_hash: Hash32 # Hash of execution block
transactions_root: Root
withdrawals_root: Root
blob_gas_used: uint64 # [New in Deneb:EIP4844]
excess_blob_gas: uint64 # [New in Deneb:EIP4844]
The relay transparently forwards body.blob_kzg_commitments
(which is controlled by the proposer) to beacon clients rather than the original commitments provided by the builder. Untrusted proposers would not receive an unblinded block as the beacon client validation will error and cause the server to reply with an error message.
On the other hand, the relay will unblind the block and send the trusted proposer back the entire payload even if an error occurs. Having received the unblinded block, a malicious proposer can re-order its contents and propose a second block with sandwiched transactions. Because the relay's block includes invalid KZG commitments, peers will reject it during data availability checks, giving the malicious proposer a better chance its block is finalized.
Timeline
- Nov 23 6:04 AM UTC Bug report sent to Gattaca’s email
- Nov 25 11:36 AM UTC Gattaca replies and asks for Telegram contact
- Nov 25 6:07 PM UTC Gattaca sends an already deployed patch that fixes the issue
Conclusion
A malicious, trusted proposer could have signed a blinded beacon block and provided the relay with invalid KZG commitments to receive the unblinded payload, proposing an alternative block that is profitable for them which will win out against the relay’s invalid block. The fix enforces that the KZG commitments sent by the proposer must be equivalent to what the relay received from the builder, as the Flashbots implementation does.
We would like to thank Gattaca for their responsiveness during our disclosure, as well as the bug bounty they offered as reward.