--- tags: eips, withdrawals, research, ethereum --- # Generalized message bus for execution layer to consensus layer communication ------------------------------ ## About `Generalized message bus` (GMB) is a generic data pipe from the execution layer of Ethereum to the consensus layer. The implementation is similar to the `DepositContract` mechanism and looks like: 1. Execution layer smart contracts emits an `event` that is conformant to generalized message bus format and contains all the necessary data to be parsed on consensus layer. 2. The consensus layer client listens to events from a fixed list of execution layer smart contracts, collects and processes events emitted by them, similarly to how they listen to deposit events now. There is a fixed list of allowed events per specific contract address. 3. For every correct `event` from a fixed allow-list of event bus smart contracts a pre-configured handler will be called, i.e. `withdrawer_exit` for `MessageBusType.withdrawer_exit` type. Message data is parsed from byte array in `data` field. ## Why With introduction of 0x1 withdrawal credentials and rise of decentralized staking pools at least two new types of messages are proposed to be sent from execution layer to the consensus layer, with potentially other new types needed as the protocol evolves. Staker-initiated validator exits are paramount to maintain trust-minimized stakeing pools ([proposal](https://ethresear.ch/t/0x03-withdrawal-credentials-simple-eth1-triggerable-withdrawals/10021)). Key rotation is a good security practice overall. If we want [0x1-type](https://github.com/ethereum/consensus-specs/pull/2149) smart contract withdrawal credentials to be first-class citizens a message bus from execution layer is needed. Implementing a decent part of business logic on execution layer and an uniform parsing logic on consensus layer clients instead of ad-hoc handling per message type would simplify the architecture of a protocol. ## Anatomy of a GMB message A message is emitted on execution layer side via EVM event mechanism from a smart-contract from an allow-list of message bus smart-contracts, then parsed, and processed on beacon chain client. #### The execution layer side Message of a GMB is a data structure: ```solidity struct Message { uint256 messageType; bytes data; } event MessageEvent(Message message); ``` Depending on type, data is parsed to a specific payload for that type. For example, a payload of SampleMessageData: ```solidity struct SampleMessageData { address sender; uint8 number; } ``` Emitting an event with a such message payload: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.7; contract IBeaconChainMessageBus { struct Message { uint256 messageType; bytes data; } event MessageEvent(Message message); } contract SampleMessageBus is IBeaconChainMessageBus { uint256 constant SAMPLE_MESSAGE_TYPE = 10; struct SampleMessageData { address sender; uint8 n; } function submit(uint8 n) external { SampleMessageData memory sampleMessageData = SampleMessageData(msg.sender, n); bytes memory encodedData = abi.encode(sampleMessageData); emit MessageEvent(Message(SAMPLE_MESSAGE_TYPE, encodedData)); } } ``` #### The consensus layer side On the consensus layer side, we propose to implement a mapping from addresses of trusted contracts to lists of the events types expected from address. Emitters to message types relation is many-to-many: different smart contracts can emit identical messages, and one emitter can send multiple types of events potentially. This mapping should be mutable, depending on the block height where the event was emitted, to give core developers an ability to limit lifetime of an emitter contract. E.g. if there was a contract to emit key rotation message introduced in block `X` but later found to be a potetential DoS vector and deprecated after block `Y`, it would be an allowed emitter for this key rotation messages only in blocks `X` to `Y`. ```python class MessageBusType(enum): SampleMessage: int = 10 SomethingElse: int = 22 ``` Each received event is validated and processed as follows: ```python class MessageBusEvent: source: AddressT message_type: MessageBusTypeT data: bytes def execution_layer_messages(block_height: int)->List[MessageBusEvent]: allowed_emitters = allowed_emitters_for_block(block_height) el_messages = collect_execution_layer_messages(block_height, allowed_emitters) #collects all suitable events for the block from all allowed emitters return el_messages def process_messages(messages: List[MessageBusEvent]) -> None: for msg in messages: if msg.message_type == MessageBusType.SampleMessage: process_sample_event(msg.data) elif msg.message_type == MessageBusType.SomethingElse: ... ``` Allowed emitters function might look like this ```python def allowed_emitters_for_block(block_height: int) -> Dict[AddressT, List[MessageBusTypeT]]: if block_height >= 777777: return { '0x0123...9': [MessageBusType.SampleMessage], '0x0987...1': [MessageBusType.SomethingElse] } elif block_height >= 666666: return { '0x0123...9': [MessageBusType.SomethingElse], } ``` Example of a parser and a handler for messages with the `SAMPLE_MESSAGE_TYPE`. ```python def parse_sample_message_payload(data: bytes) -> Tuple[AddressT, int]: address, number = abi.decode(data) return address, number def process_sample_event(data: bytes) -> None: address, number = parse_sample_message_payload(data) print(f'{address} sends {number}') ``` ## Test cases * If msg.sender not in `AllowedEmitters` for the specific block, message should not be processed * The type emitted by msg.sender should be in the list of allowed types for this address for all processed messages * Messages should be porcessed only by the specific handlers per type ## Security consideration This proposal opens up a vector for DoS attack from execution layer to consensus layer, if emitting a message on the former is cheaper than processing a message on the latter. To protect from this, expensive to process messages should be expensive to emit, or rate-limited, or both. We think that for many types of messages current execution layer tx fees are enough to prevent DoS, however. The specific additional protection mechanism should be designed on case by case basis. For example, one could just charge an additional fee: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.7; contract IBeaconChainMessageBus { struct Message { uint256 messageType; bytes data; } event MessageEvent(Message message); } contract HeavyComputing is IBeaconChainMessageBus { uint256 constant HEAVY_COMPUTING_TYPE = 11; uint256 constant MINIMAL_FEE = 10 ** 16; modifier hasFee() { require(msg.value > MINIMAL_FEE); _; } function submit(bytes memory inputData) external hasFee payable { emit MessageEvent(Message(HEAVY_COMPUTING_TYPE, inputData)); } } ``` | Threat | Mitigation(s) | | -------- | ------------- | | An attacker is able to use the GMB to organize a DoS attack on the consensus layer clients. | 1. To charge an additional fee comparable with the computational cost of processing a specific type of message. <br> 2. Set limits on the number of messages per sender/message type per epoch.| ## Simple summary Add a generic data pipe between the execution layer and the consensus layer. It allow to handle actions on the consensus layer on more flexible and simply manner, e.g. perform key rotation or staker-initiated validator exits.