From Theory to Practice: Analyzing the Mechanism of Ethereum Rollup for Implementing Censorship-Resistant Transactions
Original Title: "Introduction to the Force Inclusion Mechanism of Rollup"
Author: NIC Lin, Head of Taipei Ethereum Meetup
Just yesterday, a shocking event occurred: the Ethereum Layer 2 Linea, launched by Metamask's parent company Consensys, was actively shut down, with the official reason being to mitigate the impact of the Velocore hacking incident. This inevitably brings to mind the previous incident where the BSC chain (BNB Chain) was shut down under official coordination to reduce the losses from hacking attacks. Whenever such events are discussed, doubts about the decentralized values advocated by Web3 arise.
Of course, the core reason for the above events lies more in the imperfection of the infrastructure itself, specifically its lack of decentralization: if a chain is sufficiently decentralized, it should not be able to shut down at will. Due to the unique structure of Ethereum Layer 2, most Layer 2 solutions rely on centralized Sequencers. Although there has been increasing discourse on decentralized sequencers in recent years, considering the purpose and structure of Layer 2, we can reasonably conclude that the sequencers of Layer 2 are unlikely to be very decentralized, and may ultimately be less decentralized than the BSC chain. If this is indeed the case, what should we do?
In fact, for Layer 2, the most direct harm caused by the lack of decentralization in sequencers lies in censorship resistance and liveness. If there are very few entities (Sequencers) processing transactions, they hold absolute power over whether to serve you: they can refuse you at will, and you may have no recourse. How to solve the censorship problem of Layer 2 is clearly an important topic.
Over the past few years, various Ethereum Layer 2 solutions have proposed a variety of solutions to the censorship problem, such as the forced withdrawal and escape pod features of Loopring, Degate, and StarkEx, as well as the Force Inclusion feature of Arbitrum and other OP Rollups. These methods can impose checks on the Sequencers under certain conditions to prevent them from arbitrarily rejecting transaction requests from any user.
In today's article, NIC Lin from the Taipei Ethereum Association shares his insights, having personally experimented with the censorship-resistant transaction features of four mainstream Rollups, analyzing the mechanism design of Force Inclusion in depth from aspects such as workflow and operational methods. This is particularly valuable for the Ethereum community and large holders of substantial assets.
Transaction Censorship and Force Inclusion
Censorship resistance is very important for a blockchain; if a blockchain can arbitrarily censor and reject user-initiated transactions, it is no different from a Web2 server. Ethereum's current censorship resistance comes from its numerous Validators. If someone wants to censor Bob's transaction and prevent it from being included on-chain, they either have to bribe most of the Validators in the network or spam the entire network by continuously sending transactions with higher fees than Bob's to occupy block space. Regardless of the method, the cost will be very high.
Note: In Ethereum's current PBS architecture, the cost of censoring transactions has been significantly reduced, as can be seen in the block ratios related to OFAC's censorship of Tornado Cash transactions. The current censorship resistance relies on independent validators and relays outside the jurisdiction of OFAC and the government.
But what about Rollups? Rollups do not require a large number of Validators to ensure security; even if a Rollup has only one centralized role (Sequencer) to produce blocks, it can still be as secure as L1. However, security and censorship resistance are two different matters; even if a Rollup is as secure as Ethereum, with only one centralized Sequencer, it can censor any user's transaction.
The Sequencer can refuse to process a user's transaction, resulting in the user's funds being locked and unable to leave the Rollup.
Force Inclusion Mechanism
Rather than requiring Rollups to have a large number of decentralized Sequencers, it is better to directly leverage L1's censorship resistance:
The Sequencer is supposed to package transaction data and send it to the L1 Rollup contract, so why not add a design in the contract that allows users to insert transactions into the Rollup contract themselves? This mechanism is called "Force Inclusion." As long as the Sequencer cannot censor users at the L1 level, it cannot prevent users from forcibly inserting transactions at L1. In this way, the Rollup can inherit the censorship resistance of L1.
The Sequencer cannot censor users' L1 transactions unless at a very high cost.
How Should Forced Transactions Take Effect?
If transactions are allowed to be directly written into the Rollup contract through Force Inclusion (i.e., taking effect immediately), then the state of the Rollup will change immediately. For example, if Bob uses the Force Inclusion mechanism to insert a transaction "transfer 1000 DAI to Carol," if the transaction takes effect immediately, then in the latest state, Bob's balance will decrease by 1000 DAI, and Carol's will increase by 1000 DAI.
If Force Inclusion can directly write the transaction into the Rollup contract and take effect immediately, the state will change immediately.
If the Sequencer is also collecting transactions off-chain and sending the next batch of transactions to the Rollup contract, it is possible that the transaction forcibly inserted by Bob could affect the transactions being processed. This issue must be avoided, so Rollups generally do not allow Force Inclusion transactions to take effect immediately. Instead, they first allow users to insert transactions into a waiting queue on L1, entering a "pending" state.
When the Sequencer packages off-chain transactions and sends them to the Rollup contract, it decides whether to include the aforementioned transactions in the transaction sequence. If the Sequencer continues to ignore these transactions in the "pending" state, once the window period ends, users can forcibly insert these transactions into the Rollup contract.
The Sequencer can decide when to "add in" transactions from the waiting queue.
The Sequencer can still refuse to process transactions in the waiting queue.
If the Sequencer continues to refuse for a long time, after a certain period, anyone can forcibly insert transactions into the Rollup contract using the Force Inclusion feature.
Next, we will sequentially introduce the Force Inclusion mechanisms of four well-known Rollups: Optimism, Arbitrum, StarkNet, and zkSync.
Optimism's Force Inclusion Mechanism
First, let's introduce the Deposit process of Optimism. This Deposit not only refers to depositing money into Optimism but also includes sending "information from the user to L2" to L2. When the L2 node receives the newly deposited message, it converts the message into an L2 transaction for execution and sends it to the specified recipient.
User's message from L1 Deposit to L2
L1CrossDomainMessenger Contract
When a user wants to deposit ETH or ERC-20 tokens into Optimism, they interact with the L1StandardBridge contract on the front end, specifying how much to deposit and which L2 address will receive these assets.
The L1StandardBridge contract will pass the message to the next layer's L1CrossDomainMessenger contract, which primarily serves as a communication component between L1 and L2. The L1StandardBridge communicates with the L2StandardBridge through this generic communication component to determine who can mint tokens on L2 or who can unlock tokens from L1.
If developers need to create a contract that communicates and synchronizes state between L1 and L2, they can build it on top of the L1CrossDomainMessenger contract.
User's message is transmitted from L1 to L2 through the CrossDomainMessenger contract.
Note: In some images of this article, CrossDomainMessenger is mistakenly written as CrossChainMessenger.
OptimismPortal Contract
The L1CrossDomainMessenger contract will then send the message to the underlying OptimismPortal contract. After processing, the OptimismPortal contract will emit an event called TransactionDeposited, with parameters including "the sender," "the recipient," and relevant execution parameters.
Next, the L2 Optimism nodes will listen for the Transaction Deposited event emitted by the OptimismPortal contract and convert the parameters in the event into an L2 transaction. The initiator of this transaction will be the "sender" specified in the Transaction Deposited event parameters, and the recipient will be the "recipient" in the event parameters, with other transaction parameters also derived from the parameters in the aforementioned event.
L2 nodes will convert the parameters of the Transaction Deposited event emitted by OptimismPortal into an L2 transaction.
For example, this is a transaction where a user deposits 0.01 ETH through the L1StandardBridge contract. This message and ETH are sent to the OptimismPortal contract (address 0xbEb5…06Ed) and then converted into an L2 transaction a few minutes later:
The message initiator is the L1CrossDomainMessenger contract; the recipient is the L2CrossDomainMessenger contract on L2; the message content indicates that the L1StandardBridge has received Bob's deposit of 0.01 ETH. This will trigger additional processes, such as minting an additional 0.01 ETH for the L2StandardBridge, which will then be transferred to Bob.
How to Trigger Specifically
When you want to forcibly include a transaction into Optimism's Rollup contract, the desired effect is to ensure that a transaction "initiated from your L2 address on L2" can be executed smoothly. At this point, you should submit the message directly to the OptimismPortal contract using your L2 address (note that the OptimismPortal contract is actually on L1, but the OP address format is consistent with L1 addresses, so you can directly call the aforementioned contract using the same address as your L2 account).
Then, the initiator of the L2 transaction converted from the Transaction Deposited event will be your L2 account, and the transaction format will be the same as a normal L2 transaction.
In the L2 transaction converted from the Transaction Deposited event, the initiator will be Bob himself; the recipient is the Uniswap contract; and it will include the specified ETH, just as if Bob initiated the L2 transaction himself.
To invoke Optimism's Force Inclusion feature, you need to directly call the depositTransaction function of the OptimismPortal contract, filling in the parameters of the transaction you want to execute on L2.
I conducted a simple Force Inclusion experiment where the goal of this transaction was to perform a self-transfer using my address (0xeDc1…6909) on L2, along with a message saying "force inclusion."
This is the L1 transaction where I executed the depositTransaction function through the OptimismPortal contract. You can see that in the emitted Transaction Deposited event, both the from and to fields are myself.
The remaining value in the opaque Data field encodes "how much ETH the caller of the depositTransaction function attached," "how much ETH the L2 transaction initiator wants to send to the recipient," "the GasLimit for the L2 transaction," and "the Data for the L2 recipient," among other information.
Decoding the above information yields:
"How much ETH the caller of the depositTransaction attached": 0, because I am not depositing ETH from L1 to L2;
"How much ETH the L2 transaction initiator wants to send to the recipient": 5566 (wei);
"GasLimit for the L2 transaction": 50000;
"Data for the L2 recipient": 0x666f72636520696e636c7573696f6e, which is the hexadecimal encoding of the string "force inclusion."
Shortly after, the converted L2 transaction appeared: a transaction where I transferred money to myself on L2, with an amount of 5566 wei and Data containing the string "force inclusion." It is worth noting that in the second-to-last line of the Other Attributes, the TxnType (transaction type) shows as system transaction 126 (System), indicating that this transaction was not initiated by me on L2, but was converted from the Deposited event of the L1 transaction.
Converted L2 transaction
If you want to call an L2 contract through Force Inclusion and send different Data, you simply need to fill in the parameters one by one in the depositTransaction function. Just remember to use the same L1 address as your L2 account to call the depositTransaction function, so that when the Deposited Event is converted into an L2 transaction, the initiator will be your L2 account.
SequencerWindow
The Optimism L2 node mentioned earlier converts the Transaction Deposited event into an L2 transaction; this Optimism node refers to the Sequencer. Since this relates to transaction ordering, only the Sequencer can decide when to convert the aforementioned event into an L2 transaction.
When the Sequencer listens for the TransactionDeposited event, it does not necessarily convert the event into an L2 transaction immediately; there may be a delay. The maximum duration of this delay is called the SequencerWindow.
Currently, the Sequencer Window on the Optimism mainnet is 24 hours, meaning that when a user deposits money from L1 or Force Includes a transaction, the worst-case scenario is that it will only be included in the L2 transaction history after 24 hours.
Arbitrum's Force Inclusion Mechanism
In Optimism, the L1 Deposit operation emits a Transaction Deposited event, and the rest is waiting for the Sequencer to include the aforementioned operation. However, in Arbitrum, operations occurring on L1 (such as depositing money or sending messages to L2) are stored in a queue on L1, rather than simply emitting an event.
The Sequencer is given a period of time to include the transactions from the aforementioned queue into the L2 transaction history; if the time expires and the Sequencer has not acted, anyone can complete the inclusion on behalf of the Sequencer.
Arbitrum maintains a Queue in the L1 contract; if the Sequencer does not actively process the transactions in the Queue, anyone can forcibly include the transactions in the Queue into the L2 transaction history once the time expires.
In Arbitrum's design, operations such as deposits that occur on L1 must go through the Delayed Inbox contract, which, as the name suggests, will delay the effect of these operations. Another contract is the Sequencer Inbox, which is the direct place where the Sequencer uploads L2 transactions to L1. Each time the Sequencer uploads an L2 transaction, it can also take some pending transactions from the Delayed Inbox and write them into the transaction history.
The Sequencer can take transactions from the Delayed Inbox when writing new transactions.
Complex Design and Reference Materials
If readers refer directly to the Arbitrum official documentation regarding Sequencers and Force Inclusion, they will see a description of how Force Inclusion roughly works, along with some parameter names and function names:
Users first call the sendUnsignedTransaction function on the DelayedInbox contract. If the Sequencer does not include it within approximately 24 hours, the user can call the forceInclusion function on the SequencerInbox contract. However, the Arbitrum official documentation does not provide links to these functions, so one must look for the corresponding functions in the contract code.
Upon finding the sendUnsignedTransaction function, you may discover that you need to fill in the nonce value and maxFeePerGas value yourself. Which address's nonce? Which network's maxFeePerGas? How should you fill it in? There are no reference documents, not even Natspec. You will also find a bunch of seemingly similar functions in the Arbitrum contract:
sendL1FundedUnsignedTransaction, sendUnsignedTransactionToFork, sendContractTransaction, sendL1FundedContractTransaction, none of which have documentation explaining the differences, how to use them, or how to fill in the parameters, not even Natspec.
You might try filling in parameters and sending transactions with a trial-and-error approach to find the correct usage, but you will find that all these functions will perform Address Aliasing on your L1 address, resulting in the Sender being a completely different address when initiating the transaction on L2, leaving your L2 address unchanged.
sendL2Message
Later, by chance, I opened Google search and discovered that Arbitrum has its own tutorial library, which includes scripts demonstrating how to send L2 transactions from L1 (which is essentially the meaning of Force Inclusion). The functions listed there are completely different from any mentioned above, and one of them is called sendL2Message, and the message parameter must actually include a signed L2 transaction?
Who would know that the "message sent to L2 through Force Inclusion" would actually be a "signed L2 transaction"? Moreover, there is no documentation or Natspec explaining when and how to use this function.
Conclusion: Manually generating a forced transaction in Arbitrum is quite cumbersome. It is recommended to follow the official tutorial to run the Arbitrum SDK. Unlike other Rollups, Arbitrum lacks clear developer documentation and code annotations, and many functions' purposes and parameters are poorly explained, causing developers to spend significantly more time than expected to integrate and use it. I also inquired about Arbitrum in their Discord, but did not receive satisfactory answers.
When asking in Discord, the response was merely to look at sendL2Message, without any explanation of the functions' functionalities (even the sendUnsignedTransaction mentioned in the Force Inclusion documentation), what they are for, how to use them, or when to use them.
StarkNet's Force Inclusion Mechanism
Unfortunately, StarkNet currently does not have a Force Inclusion mechanism. There are only two articles discussing censorship and Force Inclusion on the official forum.
Unprovable Failed Transactions
The reason for this is that StarkNet's zero-knowledge proof system cannot prove a failed transaction, so it cannot allow Force Inclusion. If someone maliciously (or unintentionally) Force Includes a failed transaction that cannot be proven, StarkNet would become stuck: because once the transaction is forcibly included, the Prover must prove that the transaction failed, but it cannot do so.
StarkNet expects to introduce the ability to prove failed transactions in version v0.15.0, after which it should be able to further implement the Force Inclusion mechanism.
zkSync's Force Inclusion Mechanism
The L1->L2 message transmission and Force Inclusion mechanism of zkSync are both conducted through the MailBox contract's requestL2Transaction function, where users specify the L2 address, calldata, additional ETH amount, L2 GasLimit, etc. The requestL2Transaction will combine these parameters into an L2 transaction and place it in a priority queue (PriorityQueue), which the Sequencer will reference when packaging transactions to upload to L1 (via the commitBatches function), indicating how many transactions to include from the priority queue into the L2 transaction records.
The Force Inclusion form of zkSync is very similar to Optimism, where the initiator's L2 address (consistent with the L1 address) calls the relevant function and fills in the information (recipient, calldata, etc.), rather than filling in a signed L2 transaction as in Arbitrum; however, in design, it is similar to Arbitrum, maintaining a queue on L1 to manage user-submitted L2 transactions or messages to L2.
If you use zkSync's official bridge to deposit ETH, like this transaction, it calls the MailBox contract's requestL2Transaction function, which will place this deposit ETH L2 transaction into the priority queue and emit a NewPriorityRequest event. Since the contract encodes the L2 transaction data into a bytes string, it is not easily readable. However, if you look at the parameters of this L1 transaction, you will see that the L2 recipient is also the initiator of the transaction (since it is a deposit to oneself), so after a while, when this L2 transaction is taken from the priority queue by the Sequencer and included in the transaction history, it will be converted into a transaction where the initiator transfers to themselves on L2, with the transfer amount being the ETH amount included in the user's L1 deposit transaction.
In the L1 Deposit transaction, both the initiator and recipient are 0xeDc1…6909, with an amount of 0.03 ETH, and calldata is empty.
On L2, there will be a transaction where 0xeDc1…6909 transfers to themselves, with the transaction type (TxnType) being 255, indicating it is a system transaction.
Next, I directly called the zkSync requestL2Transaction function, similar to my previous experiment with OP's forced transaction feature, to send a self-transfer: without any ETH, with calldata containing the hexadecimal encoding of the string "force inclusion."
Then it was converted into a transaction on L2 where I transferred to myself, with calldata containing the hexadecimal string of "force inclusion": 0x666f72636520696e636c7573696f6e.
When the Sequencer takes the transaction from the PriorityQueue and writes it into the transaction history, it will be converted into the corresponding L2 transaction.
Through the requestL2Transaction function, users can submit information using the same L1 account as their L2 address, specifying the L2 recipient, additional ETH amount, and calldata. If users want to call other contracts or include different Data, they simply fill in the parameters one by one in the requestL2Transaction function.
No Function for Users to Force Inclusion Yet
Although the L2 transaction is placed in the priority queue, and the waiting period for the Sequencer to include this L2 transaction is calculated, currently, zkSync's design does not provide a Force Inclusion function that allows users to force execution, which means it is only a half-measure. While there is a "waiting period for inclusion," it ultimately still depends on "whether the Sequencer wants to include it": the Sequencer can wait until the expiration to include it or may never include any transactions from the priority queue.
In the future, zkSync should add relevant functions to allow users to forcibly include transactions into the L2 transaction history after the inclusion validity period has expired but has not yet been included by the Sequencer, thus achieving a truly effective Force Inclusion mechanism.
Conclusion
L1 relies on numerous validators to ensure the network's "security" and "censorship resistance," while Rollups, which are written by a few or even a single Sequencer, have weaker censorship resistance. Therefore, Rollups need a Force Inclusion mechanism to allow users to bypass the Sequencer and write transactions into history, avoiding the inability to use the Rollup or withdraw funds due to censorship by the Sequencer.
Force Inclusion allows users to forcibly write transactions into history, but the design must choose between "whether the transaction can be immediately inserted into history and take effect immediately." If transactions are allowed to take effect immediately, it will negatively impact the Sequencer, as all transactions waiting to be included on L2 may be affected by transactions forcibly included from L1.
Thus, the current Force Inclusion mechanisms of Rollups typically allow transactions inserted on L1 to enter a waiting state first, giving the Sequencer a window of time to respond and decide whether to include these pending transactions.
Both zkSync and Arbitrum maintain a queue on L1 to manage user-submitted L2 transactions or messages to L2. Arbitrum calls it DelayedInbox; zkSync calls it PriorityQueue.
However, the way zkSync submits L2 transactions is more similar to Optimism, where messages are sent from the L2 address to L1, so that when converted into L2 transactions, the initiator will be that L2 address. The function for sending L2 transactions in Optimism is called depositTransaction; in zkSync, it is called requestL2Transaction. In contrast, Arbitrum generates a complete signed L2 transaction and sends it via the sendL2Message function, where Arbitrum will restore the signer as the initiator of the L2 transaction.
Currently, StarkNet does not have a Force Inclusion mechanism; zkSync has implemented a half-measure of Force Inclusion, --- there is a PriorityQueue and each L2 transaction in the Queue has an inclusion validity period, but this validity period is currently merely decorative, as the Sequencer can choose not to include any L2 transactions from the PriorityQueue at all.