Introduction to the Force Inclusion Mechanism of Rollup
Yesterday, something shocking happened: Linea, the Layer 2 solution of Ethereum developed by Consensys, the parent company of Metamask, voluntarily shut down. The official statement claimed that this was done to minimize the impact of the Velocore hack. This incident reminds people of the previous shutdown of BNB Chain by BSC to mitigate the losses from a hack. Whenever such incidents occur, people start to question the decentralization values advocated by Web3.
Of course, the core reason behind these incidents lies in the imperfect infrastructure itself, namely the lack of decentralization. If a chain is decentralized enough, there should be no need to shut it down. Due to the unique structure of Ethereum Layer 2, most Layer 2 solutions rely on centralized sequencers. Although there has been increasing discussion about decentralized sequencers in recent years, considering the purpose and structure of Layer 2, it is highly likely that Layer 2 sequencers will not be as decentralized as the BSC chain. If this is indeed the case, what should we do?
For Layer 2 solutions, the lack of decentralization in sequencers poses a direct threat to censorship resistance and liveliness. If the entity responsible for processing transactions (Sequencer) is limited in number, it holds absolute power in deciding whether to serve you or not. It can reject your transactions without you having any recourse. How to address the censorship resistance issue of Layer 2 is clearly an important topic.
In the past few years, various Ethereum Layer 2 solutions have proposed different approaches to address the censorship resistance issue. These include Loopring, Degate, StarkEx’s forced withdrawal and escape pod, Arbitrum, and other OP Rollup solutions with Force Inclusion functionality. These methods can provide checks and balances on sequencers under certain conditions, preventing them from arbitrarily rejecting transaction requests from users.
In today’s article, NIC Lin from Taipei Ethereum Association personally experimented with the censorship-resistant transaction features of four mainstream Rollups and analyzed the mechanism design of Force Inclusion in terms of workflow and operational methods. This is particularly valuable for the Ethereum community and large holders of assets.
Censorship Resistance and Force Inclusion in Transactions
Censorship resistance is crucial for a blockchain. If a blockchain can censor and reject transactions initiated by users at will, 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 transactions and prevent them from being included in the chain, they either need to bribe most of the validators in the network or spam the network with junk transactions with higher fees than Bob’s to occupy block space. Both methods are costly.
Note: In Ethereum’s current PBS architecture, the cost of censoring transactions is reduced. You can refer to the proportion of blocks that underwent OFAC review for 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 is still secure like Layer 1. However, security and censorship resistance are two different things. Even if a Rollup is as secure as Ethereum, with only one centralized Sequencer, it can censor any user’s transactions.
The Sequencer can refuse to process a user’s transaction, resulting in the user’s funds being stuck and unable to leave the Rollup.
Force Inclusion Mechanism
Instead of requiring Rollups to have a decentralized Sequencer, it is better to leverage the censorship resistance capability of Layer 1:
Since the Sequencer’s role is to package and send transaction data to the Rollup contract on Layer 1, why not add a design to 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 on Layer 1, it cannot prevent users from forcefully inserting transactions on Layer 1. This way, Rollup can inherit the censorship resistance capability of Layer 1.
How Should Forced Transactions Take Effect?
If transactions can be directly written into the Rollup contract through Force Inclusion (i.e., take effect immediately), the state of the Rollup will change immediately. For example, if Bob inserts a transaction through the Force Inclusion mechanism that says “Transfer 1000 DAI to Carol” on Layer 2, if the transaction takes effect immediately, the latest state will show that Bob’s balance is reduced by 1000 DAI and Carol’s balance is increased by 1000 DAI.
If the Sequencer is also collecting transactions off-chain and sending the next batch of transactions to the Rollup contract, the transaction forcefully inserted and immediately effective by Bob can affect these subsequent transactions. This problem needs to be avoided, so Rollups generally do not allow Force Inclusion transactions to take effect immediately. Instead, users are required to insert the transactions into the waiting queue on Layer 1, entering a “preparation” state.
When the Sequencer packages off-chain transactions and sends them to the Rollup contract, it decides whether to include the above-mentioned transactions in the transaction sequence. If the Sequencer continues to ignore these transactions in the “preparation” state, when the window period ends, users can forcefully insert these transactions into the Rollup contract.
SequencerWindow
Optimism’s Force Inclusion Mechanism
First, let’s introduce the deposit process of Optimism, which not only refers to depositing money into Optimism but also includes “sending messages from users to L2.” When an L2 node receives a new deposit message, it converts the message into an L2 transaction to be executed and sends it to the specified recipient.
When a user wants to deposit ETH or ERC-20 tokens into Optimism, they interact with the L1StandardBridge contract through a frontend webpage on Layer 1, specifying the amount to deposit and the L2 address that will receive the assets.
The L1StandardBridge contract passes the message to the L1CrossDomainMessenger contract, which primarily serves as a component for communication between Layer 1 and Layer 2. The L1StandardBridge communicates with the L2StandardBridge on Layer 2 through this common communication component to determine who can mint tokens on Layer 2 or unlock tokens from Layer 1.
If a developer needs to develop a contract that communicates and synchronizes states between Layer 1 and Layer 2, they can build it on top of the L1CrossDomainMessenger contract.
The L1CrossDomainMessenger contract then sends the message to the bottom-level OptimismPortal contract. After processing, the OptimismPortal contract emits an event called TransactionDeposited, which includes the “sender” and “recipient” of the message, as well as relevant execution parameters.
Next, the Optimism node on Layer 2 listens for the TransactionDeposited event emitted by the OptimismPortal contract and converts the event parameters into an L2 transaction. The initiator of this transaction will be the “sender” specified in the TransactionDeposited event parameters, and the recipient will be the “recipient” specified in the event parameters. Other transaction parameters are also derived from the parameters in the event.
For example, if a user deposits 0.01 ETH into Optimism through the L1StandardBridge contract, the message and ETH will be passed to the OptimismPortal contract (address: 0xbEb5…06Ed) and converted into an L2 transaction a few minutes later:
The initiator of the message is the L1CrossDomainMessenger contract, and the recipient is the L2CrossDomainMessenger contract on Layer 2. The message content is that the L1StandardBridge has received Bob’s deposit of 0.01 ETH. This will trigger additional processes, such as minting 0.01 ETH for L2StandardBridge and transferring it to Bob.
How to Trigger It
When you want to forcefully include a transaction in the Rollup contract of Optimism, you need to ensure that a transaction “from your L2 address to be initiated and executed on L2” can be executed smoothly. To achieve this, you should use your L2 address to directly submit the message to the OptimismPortal contract (note that the OptimismPortal contract is actually on Layer 1, but the OP address format is the same as the L1 address format, so you can directly call the contract with the L1 account that has the same address as your L2 account).
The L1CrossDomainMessenger contract will then emit a TransactionDeposited event, and the converted L2 transaction from this event will have your L2 account as the initiator, making the transaction format the same as a normal L2 transaction.
If you want to call L2 contracts and send different Data through Force Inclusion, you only need to fill in the parameters in the depositTransaction function mentioned above. However, remember to use the L1 address that is the same as your L2 account to call the depositTransaction function. This way, when the Deposited event is converted into an L2 transaction, the initiator will be your L2 account.
SequencerWindow
The Optimism L2 node converts the TransactionDeposited event into an L2 transaction. In fact, this Optimism node listens for the Transaction Deposited event emitted by the OptimismPortal contract and converts the event parameters into an L2 transaction. This transaction will have your L2 account as the initiator.As a professional translator, I will translate this news article into English using descriptive language. The sentences will be accurate, smooth, and will retain proper nouns and all
references. I will ensure that the meaning remains the same and there are no grammatical errors. Please note that I will not include a period at the end of the translation.
The ism node refers to the Sequencer, as it is related to transaction ordering, so only the Sequencer can determine when the aforementioned events should be converted into L2 transactions.
When the TransactionDeposited event is detected, the Sequencer may not immediately convert the event into an L2 transaction. There can be a delay, and the maximum duration of this delay is known as the SequencerWindow.
Currently, the Sequencer Window on the Optimism mainnet is set at 24 hours. This means that in the worst-case scenario, when a user deposits money from L1 or forces the inclusion of a transaction, it may take up to 24 hours for it to be included in the L2 transaction history.
Arbitrum’s Force Inclusion mechanism
In Optimism, the L1 Deposit operation throws a Transaction Deposited event, and the rest is waiting for the Sequencer to include the operation. However, in Arbitrum, operations that occur on L1 (such as depositing money or sending messages to L2) are stored in a queue on L1 instead of simply throwing an event.
The Sequencer is given a certain amount of time to include the transactions from the queue into the L2 transaction history. If the time expires and the Sequencer has not done so, anyone can complete the task 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.
In Arbitrum’s design, operations that occur on L1, such as deposits, must go through the Delayed Inbox contract. As the name suggests, operations here are delayed. Another contract, the Sequencer Inbox, is where the Sequencer directly 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 include them in the transaction history.
The Sequencer can take transactions from the Delayed Inbox and include them when writing new transactions.
Complex design and lack of detailed references
If readers refer directly to the official Arbitrum section on Sequencer and Force Inclusion, they will see a rough explanation of how Force Inclusion works, as well as some parameter and function names. Users need to call the sendUnsignedTransaction function in the Delayed Inbox contract. If the Sequencer does not include it within about 24 hours, the user can call the forceInclusion function in the Sequencer Inbox contract. However, the Arbitrum official website does not provide links to these functions; users need to refer to the corresponding functions in the contract code themselves.
When users find the sendUnsignedTransaction function, they will realize that they need to fill in the nonce value and the maxFeePerGas value. Which address does the nonce belong to? Which network does maxFeePerGas belong to? How should they be filled in? There are no documents or even Natspec explanations. Furthermore, users will find a bunch of similar-looking functions in the Arbitrum contract:
sendL1FundedUnsignedTransaction, sendUnsignedTransactionToFork, sendContractTransaction, sendL1FundedContractTransaction. There are no documents explaining the differences between these functions, how to use them, or how to fill in the parameters. There are no Natspec explanations either.
With a tentative attitude, you try to fill in the parameters and submit the transaction, hoping to find the correct usage through trial and error. However, all these functions will alias your L1 address, resulting in a different sender address when initiating transactions on L2. As a result, your L2 address remains unchanged.
sendL2Message
Later, by chance, you open a Google search and discover that Arbitrum has its own Tutorial library. It contains scripts demonstrating how to send L2 transactions from L1 (which means Force Inclusion). The functions listed in the tutorial are completely different from the ones mentioned above. Instead, there is a function called sendL2Message, and the message parameter must be a transaction signed with an L2 account.
Who would have known that the “message sent to L2 through Force Inclusion” would actually be a “signed L2 transaction”? And there are no documents or Natspec explanations on when and how to use this function.
Conclusion: It is quite challenging to manually generate a forced transaction in Arbitrum. It is recommended to follow the official Tutorial and run the Arbitrum SDK. Unlike other Rollups, Arbitrum lacks clear developer documentation and code annotations, making it time-consuming for developers to integrate and use it. Even when I asked the Arbitrum team on the Arbitrum Discord, I did not receive satisfactory answers.
When I asked on Discord, they only told me to look at sendL2Message and did not explain the functionality of other functions (including sendUnsignedTransaction mentioned in the Force Inclusion documentation), 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.
Inability to prove failed transactions
The reason mentioned above is that StarkNet’s zero-knowledge proof system cannot prove a failed transaction, so Force Inclusion is not allowed. If someone maliciously (or unintentionally) force includes a failed transaction that cannot be proven, StarkNet will be stuck because the Prover must prove the failed transaction after it is forcibly included, but it cannot do so.
StarkNet is expected to introduce the ability to prove failed transactions in version v0.15.0, which will further enable the Force Inclusion mechanism.
zkSync’s Force Inclusion mechanism
The L1->L2 message transmission and Force Inclusion mechanisms in zkSync are carried out through the requestL2Transaction function of the MailBox contract. Users specify the L2 address, calldata, additional ETH amount, L2GasLimit value, etc. The requestL2Transaction function combines these parameters into an L2 transaction and puts it into a priority queue (PriorityQueue). The Sequencer will include a certain number of transactions from the priority queue into the L2 transaction history when packing and uploading the transactions to L1 (via the commitBatches function).
The Force Inclusion process in zkSync is similar to Optimism. Users use their L2 address (which is the same as their L1 address) to call the relevant function, and fill in the data (callee, calldata, etc.), rather than using a signed L2 transaction like Arbitrum. However, the design is similar to Arbitrum, as both maintain a queue (Queue) on L1 and the Sequencer takes user-submitted pending transactions from the queue and includes them in the transaction history.
If you use zkSync’s official bridge to Deposit ETH, for example, this transaction will call the requestL2Transaction function of the MailBox contract. It will put this Deposit ETH L2 transaction into the priority queue and throw a NewPriorityRequest event. Since the contract encodes the L2 transaction data into a string of bytes, it is not easy to read. If you look at the parameters of this L1 transaction, you will see that the recipient of the L2 transaction is also the initiator of the transaction (because it is a self-deposit), so after a while, this L2 transaction will be taken out by the Sequencer from the priority queue and included in the transaction history. On L2, it will be transformed into a transaction where the initiator transfers to themselves, and the amount transferred will be the ETH amount carried in the Deposit ETH transaction on L1.
In the L1 Deposit transaction, both the initiator and the recipient are 0xeDc1…6909, and the amount is 0.03 ETH, with empty calldata.
An L2 transaction will appear where 0xeDc1…6909 transfers to themselves. The transaction type (TxnType) is 255, which means a system transaction. As an experiment, I used the forced transaction function just like the previous experiment with OP. I called zkSync’s requestL2Transaction function and sent a self-transfer: no ETH was included, and the calldata contained the HEX encoding of the string “force inclusion.” It was then transformed into an L2 transaction where the initiator transfers to themselves, and the calldata contains the hexadecimal string “force inclusion”: 0x666f72636520696e636c7573696f6e.
When the Sequencer takes the transaction from the priority queue and includes it in the transaction history, it will be transformed into the corresponding L2 transaction.
Through the requestL2Transaction function, users can submit data on L1 using an L1 account that is the same as the L2 address and specify the L2 recipient, the additional ETH amount, and calldata. If users want to call other contracts with different data, they can fill in the parameters one by one in the requestL2Transaction function.
No user-enforced inclusion feature yet
Although the L2 transaction is placed in the priority queue and its inclusion waiting period is calculated, the current design of zkSync does not include a user-enforced Force Inclusion function, which means it is only halfway there. While there is a “waiting period for inclusion,” in reality, it is still “up to the Sequencer whether to include it”: the Sequencer can wait until the expiration and still choose not to include any transactions from the priority queue.
In the future, zkSync is expected to add relevant functions that will allow users to forcibly include transactions into the L2 transaction history when the inclusion waiting period has expired but they have not been included by the Sequencer. This will be a truly effective Force Inclusion mechanism.
Summary
L1 relies on a large number of validators to ensure network “security” and “censorship resistance.” Rollups, on the other hand, rely on a few or even a single Sequencer to write transactions, making them less resistant to censorship. Therefore, Rollups need a Force Inclusion mechanism that allows users to bypass the Sequencer and write transactions into the history to avoid being unable to use the Rollup or withdraw funds.
Force Inclusion allows users to forcibly write transactions into the history, but the design requires a choice regarding whether transactions can be immediately inserted into the history and take effect. If transactions are allowed to take immediate effect, it will have a negative impact on the Sequencer, as any L1 transaction that forcibly enters the history can potentially affect the L2 transactions waiting to be included.
Therefore, the current Force Inclusion mechanisms in Rollups first put L1 transactions into a waiting state and give the Sequencer a time window to react and decide whether to include these waiting transactions.
Both zkSync and Arbitrum maintain a Queue on L1 to manage user-submitted L2 transactions or messages to L2. Arbitrum calls it the DelayedInbox, while zkSync calls it the PriorityQueue.
However, the way zkSync sends L2 transactions is more similar to Optimism. Both use L2 addresses to send messages to L1, and the resulting L2 transactions have the initiator as the L2 address. Optimism’s function for sending L2 transactions is called depositTransaction, while zkSync’s function is called requestL2Transaction. Arbitrum, on the other hand, generates a complete L2 transaction, signs it, and sends it through the sendL2Message function. Arbitrum restores the sender on L2 based on the signature.
StarkNet currently does not have a Force Inclusion mechanism. zkSync has a mechanism similar to Force Inclusion, with the PriorityQueue and a waiting period for each L2 transaction in the queue. However, at the moment, this waiting period is merely decorative, as the Sequencer can choose not to include any transactions from the PriorityQueue at all.