Code Modifications
This section describes the main modifications that have to be implemented on top of the existing OP Stack and Bedrock codebase. In addition, it describes the required system contracts.
Shutter Inbox Contract
The central system contract is the Shutter Inbox Contract. It exposes the commit function which can be called by standard Ethereum transactions. In the following, those are named Commit Transactions.
The user encrypts a payload, consisting of data, value and to fields, and attaches this in serialized form as an argument to the commit call. Additional arguments are the future block-number when the transaction has to be executed as well as the estimated gas-limit for the execution. The Commit Transaction has to include an ETH-transfer to the contract via the transaction’s value field, so that the amount is equal to the gas-limit argument multiplied by the gas-price of the Commit Transaction.
The contract will store a FIFO-queue of the passed in arguments together with the sender address in its storage. The contract makes sure that the cumulative gas-limits of the queue can never surpass the block-gas-limit, otherwise the contract call fails. In order to keep the total state-growth constant, the contract will only accept and enqueue transactions where the block-number argument equals the next block number, and the contract will delete all previous transactions from the queue once they are handled in the Reveal Transaction. The accumulated gas transferred to the contract via the Commit Transactions can be withdrawn by a configurable account, e.g. the sequencer or a DAO.
Keyper Set Contract
The Keyper Set Contract manages the current keyper set. It allows an owner, e.g. the OP DAO, to add and remove members at will at certain block numbers. The keyper set contract also defines an emergency shutdown mechanism described in more detail in the “Potential Issues and Solutions” section.
Key Broadcast Contract
The Key Broadcast Contract is a simple billboard that allows any keyper set to broadcast its eon public key after they generated it. Users can fetch it from there.
Engine API
In order to include the decryption key in the block and make it available in the state-transition-function (STF), it has to be passed from the receiving end (op-node) to the block-building engine in op-geth. Therefore the EngineAPI payload-attributes for the engine_forkChoiceUpdatedV1 have to be extended to include a decryption-key field.
op-node
The op-node is responsible for initiating the construction and release of new blocks in the op-geth node in regular intervals. It does so by continuously adding transactions to the block candidate at all times. This process now has to be paused briefly after each proposed block until the keypers have produced the decryption key.
Concretely, the time between a sequencer’s unsafe head update and the successful keyper-decryption process blocks the execution of the next call to engine_forkChoiceUpdatedV1. Since the decryption key generation time is variable, the op-node adjusts the time for successive calls to engine_getPayloadV1 / engine_newPayloadV1 in order to keep the overall block time constant.
The op-node additionally communicates with the set of keypers by operating a libp2p module that connects to the keypers and subscribes to a decryption-key topic via the gossipsub protocol. The op-node has to have access to the current layer 2 blockchain state, in order to verify keyper-set membership and the validity of received decryption keys.
op-geth
The miner in op-geth is responsible for assembling new blocks. To do so, it picks transactions from the sorted mempool as well as deposit-transactions received from the op-node via the payload-attributes of the EngineAPI.
Block-building is initiated by the op-node via the Engine API. In this process, additional constraints have to be fulfilled and considered for block validity. It requires the decryption key for transactions included in the previous block, which the miner receives via the payload-attributes from the op-node.
The first transaction to be executed in the block’s state-transition function is the special Reveal Transaction. It is exclusively assembled by the miner and contains the decryption key. It fetches the encrypted payloads for this block from the Shutter Inbox Contract and decrypts them. Subsequently, it executes it, taking the corresponding metadata fields sender and gas limit into account. Note that the gas for the transaction has already been paid for by Commit Transaction.
After successful execution of the decrypted payload, the resulting events of all decrypted transactions are included in the receipt of the Reveal Transaction. In the remainder of the block, the miner includes new transactions from the mempool as usual, including Commit Transactions for the next block. The miner takes the execution gas limit of the latter into account in the scoring function used for mempool ordering.
Sequence diagram of the block building process. Two iterations of block building are shown - in the first, only the inclusion of normal transactions is shown in detail. In the second, only the inclusion of the Reveal Transaction is shown in detail. “NextBlock” represents the current locally built block within the sequencer’s memory. “Layer2State” represents the publicly visible unsafe head state of the layer 2 blockchain. (high res)
User Interaction
The user experience of using encrypted transactions is very similar compared to normal ones, and involves only a few additional steps in the transaction submission and status notification process. When using a specific Dapp frontend, an action that posts a transaction to the blockchain will be handled in the following way:
The user will be informed beforehand that the gas mechanism of this transaction is handled differently and the value transferred to the inbox contract is a pre-payment of gas for execution of the revealed transaction. In the background, the Dapp will locally handle the gas estimation of the payload, eventually fetch the relevant encryption inputs from the L2 chain, and encrypt the payload.
The Dapp then constructs a normal Ethereum transaction that calls the commit function of the Shutter Inbox Contract and adds the calculated execution-gas to the transaction’s value field.
Next, the Dapp will ask the user’s wallet to send the transaction, which in turn will prompt the user to sign it. Finally, the wallet will submit the signed transaction to the Optimism JSON RPC endpoint.
The Dapp will then continuously show the current execution status of the user’s transaction – but execution confirmation will take longer than usual: As a first pre-confirmation, the Dapp will check the layer 2 chain for successful inclusion of the Shutter Inbox call in the latest block. As soon as the Commit Transaction has been included, the user will see the status of the transaction as “committed, waiting for decryption and execution”.
Once the corresponding decrypted payload is found in the Reveal Transaction of the next block and executed successfully, the user will see the status of the transaction as “revealed, successfully decrypted and executed”. The Dapp may read the receipt of the Reveal Transaction and construct more detailed information on the transaction’s execution. Among others, this receipt indicates if execution was successful or if it reverted, e.g. due to running out of gas.
In case the Reveal Transaction receipt does not contain a trace of the decrypted payload, it was invalid.
Interaction With Decentralized Sequencers
Decentralized Sequencer and MEVA designs are largely orthogonal to this proposal. The only requirements for Shutterized Optimism on the block proposal mechanism are that they come with a certain degree of finality, or achieve it quickly over time (without additional blocks).
Finality Assumption
The system assumes that unsafe heads are final, as the keypers release the decryption key immediately upon observing them. If they are not, i.e., if a malicious sequencer can create a competing fork, this sequencer would be able to frontrun. Note that this attack would be both provable and attributable. We therefore recommend that the sequencer has to deposit a stake that will be slashed in case the sequencer attempts this attack.
Potential Issues and Solutions
Liveness Failures
The main failure to consider is a liveness failure caused by the keypers not producing the decryption key. This would prevent the sequencer from proposing the next valid block. For this scenario to take place, Shutter’s threshold assumption has to break: More than 1 - t keypers have to be offline, where t is the threshold parameter.
To recover from this and similar types of failure, Shutter can be disabled by a smart contract emergency switch. In that case, the rollup’s operation would fall back to standard Optimism mode. In particular, this would allow the sequencer to produce a block without including the decryption key, so that the system can continue to make progress. This cannot be used to frontrun as encrypted transactions will never be decrypted nor executed. Various options exist for which entities could trigger the emergency switch: The sequencer, OP governance, Shutter governance, or a subset of the keyper set itself. In addition, the switch could be conditional on the duration in which no block has been produced.
Latency
Encrypted transactions are executed over the course of two blocks (commit in some block, reveal in the next). Naturally, their latency until execution increases by a factor of two. Latency until inclusion of the Commit Transaction is still only a single block (note that inclusion guarantees later execution). Standard transactions are largely unaffected by Shutter, so they will still be included and executed in the next block.
However, building a block now involves generating the decryption key. Since this is carried out in a distributed manner over a P2P network, the default block time may have to be increased. The exact number will depend on the number of keypers and network latency, but a cautiously optimistic estimate is 4s.
Sequencer Side-Channel Attack
The design involves a potential issue related to the sequencer’s ability to freely choose block attributes. This control enables the sequencer to affect the execution path of previously committed transactions at a time when they already know the decryption key. This allows them to frontrun.
For instance, the sequencer may include a transaction at the top of the block that buys a token on a DEX if the block timestamp is even and sells it if it is odd. As soon as they receive the decryption key, they can decrypt the other transactions, and learn about the resulting price movement over the course of the block. By choosing the timestamp for the next block accordingly (even if it increases, odd if it decreases), they can effectively frontrun.
The sequencer can thus potentially gain an unfair advantage by executing transactions based on privileged information before other participants.
As a possible prevention mechanism, the sequencer has to commit to all variable block parameters already in the previous block. In order to prevent any degrees of freedom in the block attributes, the sequencer has to pre-commit to the block attributes difficulty, layer1-origin-hash, coinbase, and timestamp.
However, this step has to be carefully considered in order to make sure normal operation is not negatively affected.
Design Options
Block or Transaction Keys
Shutter uses an identity-based encryption scheme. This means that the encryption key is derived from the eon public key and an identity, an arbitrary parameter. This parameter is also used when the corresponding decryption key is generated. There are two major options how to choose the identity:
- one identity per block (e.g., the block number)
- one identity per transaction (e.g., a custom nonce)
The advantage of block keys is efficiency: Only a single decryption key per block has to be generated and included in each block. However, they have a negative UX impact: Before sending a transaction, users have to figure out what the identity value of the next block will be (or rather, the identity value of the block in which they want their payload to be executed). This can be difficult if network latency is high and may lead to higher confirmation times if users have to estimate more conservatively. In addition, encrypted payloads will be revealed even if the transaction is not included at all, e.g., due to high latency, network congestion, or a malicious sequencer. Note however, that an already revealed transaction can not be included later on, so this does not make the transaction susceptible for frontrunning.
Transaction keys, on the other hand, can be derived purely locally without any knowledge over the state of the system. This makes the user experience maximally straightforward and prevents revelation without inclusion. On the other hand, the system is less efficient because for each transaction a separate key has to be included.
Future Considerations
For the purpose of this proposal, practicality and low implementation complexity were primary goals. When these become less important in future versions, more options open up. Here we describe three.
Instead of including encrypted transactions in the chain and decrypting them during execution, it is possible to do the opposite: Only commit to a hash of all encrypted payloads, and at time of execution only include the decrypted payloads. As part of the state transition function, the payloads would be encrypted again in order to check correctness against the commitment. This approach is more efficient: Decrypted transactions can be compressed much better than encrypted ones, so they use less of the expensive L1 space.
The main drawback of this, however, is a much higher complexity in the sequencer: They need to keep track of which transactions they have committed to in the previous block. If they lose this data, e.g., during a crash at an inconvenient time, they will be unable to produce a valid block and the system is stuck.
Zero knowledge proofs are another potential tool to increase efficiency: A zkSNARK that proves correct decryption would allow omitting the decryption keys from the chain altogether, thus severely limiting the L1 footprint of the system, in particular if transaction keys instead of block keys are chosen. Furthermore, users could use zero knowledge proofs to make statements about their account balances without revealing the sender account, which in the current system is leaked to potential attackers. Unfortunately, zero knowledge technology is still somewhat complex.
Lastly, a potential modification to make reorg attacks by the sequencer much harder is to involve the keyper committee: In addition and simultaneous to the decryption key generation, they could produce a threshold signature on the current head of the chain. The state transition function would check this signature in order to make sure that no reorg has happened. In order to successfully attack, the sequencer would thus not only have to produce a competing block, but also require the keypers to sign it off.
Conclusion
In this document, we have proposed modifications to the OP Stack to provide front-running protection and censorship resistance to its users. The required changes are relatively small in scale, not overly complex, and viable to implement.
We have evaluated failure cases and outlined robust solutions for such scenarios, with the legacy system always serving as a fallback point to recover to, even in the worst-case.
Notably, the proposed architecture does not compromise the user experience of normal transactions, while also providing an only slightly diminished user experience for front-running protected transactions. Importantly., it is possible to submit encrypted transactions with standard wallets, requiring only small frontend modifications that can be implemented straightforwardly by using a provided library.
Overall, the outlined software architecture lays a solid foundation for a front-running protection system, balancing practicality, complexity, and security.