Just yesterday, a shocking event occurred: Linea, the Ethereum Layer 2 solution developed by Consensys, the parent company of Metamask, was proactively shut down. The official reason given was to mitigate the impact of the Velocore hacking incident. This incident inevitably brings to mind a previous case where the BSC chain (BNB Chain) was also shut down under official coordination to minimize hacking losses. These events often lead people to question the decentralized values that Web3 advocates.
The core reason behind the aforementioned events lies in the imperfection of the infrastructure, specifically its lack of decentralization. If a blockchain were sufficiently decentralized, it shouldn’t be able to shut down so easily. Due to the unique structure of Ethereum Layer 2, most Layer 2 solutions rely on centralized sequencers. Despite the growing discourse on decentralized sequencers in recent years, given the purpose and structure of Layer 2, we can assume that Layer 2 sequencers are unlikely to achieve high levels of decentralization. In fact, they may end up being less decentralized than the BSC chain. If this is the case, what can be done? For Layer 2, the most immediate risks of non-decentralized sequencers are a lack of censorship resistance and liveness. If there are only a few entities processing transactions (sequencers), they have absolute power over whether to serve you or not: they can refuse your transactions at will, leaving you without recourse. Addressing the censorship resistance issue in Layer 2 is clearly an important topic. Over the past few years, various Ethereum Layer 2 solutions have proposed different approaches to tackle this issue. For instance, Loopring, Degate, and StarkEx have introduced forced withdrawal and escape hatch functions, while Arbitrum and other Optimistic Rollups have implemented Force Inclusion features. These mechanisms can impose checks on the sequencers to prevent arbitrary refusal of user transactions. In today’s article, NIC Lin from the Taipei Ethereum Association shares his firsthand experience, experimenting with the censorship-resistant transaction features of four major Rollups and providing an in-depth analysis of the Force Inclusion mechanism, focusing on workflow and operational methods. This analysis is especially valuable for the Ethereum community and large asset holders.
Censorship resistance in transactions is crucial for any blockchain. If a blockchain can arbitrarily censor and reject user transactions, it is no different from a Web2 server. Ethereum’s transaction censorship resistance is currently ensured by its numerous validators. If someone wants to censor Bob’s transaction and prevent it from being included in the blockchain, they would either have to bribe the majority of the network’s validators or spam the network with garbage transactions that have higher fees than Bob’s, thus occupying block space. Both methods are extremely costly.
Note: In Ethereum’s current Proposer-Builder Separation (PBS) architecture, the cost of censoring transactions is significantly reduced. For example, you can look at the proportion of blocks that comply with OFAC’s censorship of Tornado Cash transactions. The current censorship resistance relies on independent validators and relays that are outside the jurisdiction of OFAC and other government entities.
But what about Rollups? Rollups do not require a large number of validators to ensure security. Even if a Rollup has only one centralized entity (Sequencer) producing blocks, it remains as secure as Layer 1 (L1). However, security and censorship resistance are two different matters. A Rollup, while being as secure as Ethereum, can still censor any user’s transaction if it only has a single centralized sequencer.
Sequencer can refuse to process a user’s transaction, resulting in the user’s funds being locked and unable to leave the Rollup.
Instead of requiring Rollups to have a large number of decentralized sequencers, it is more effective to directly leverage the censorship resistance of Layer 1 (L1):
Since the sequencer needs to package transaction data and send it to the Rollup contract on L1, we can add a feature in the contract that allows users to insert their transactions into the Rollup contract themselves. This mechanism is known as “Force Inclusion.” As long as the sequencer cannot censor users at the L1 level, it cannot prevent users from forcibly inserting transactions at L1. This way, the Rollup can inherit the censorship resistance of L1.
Sequencer cannot review the user’s L1 transactions without paying a high cost
If transactions are allowed to be directly written into the Rollup contract through Force Inclusion (meaning they take effect immediately), the state of the Rollup will change instantly. For example, if Bob uses the Force Inclusion mechanism to insert a transaction that transfers 1000 DAI to Carol, and the transaction takes effect immediately, Bob’s balance would decrease by 1000 DAI, while Carol’s balance would increase by 1000 DAI in the updated state.
If Force Inclusion allows transactions to be directly written into the Rollup contract and take effect immediately, the state of the Rollup would change instantly. If the sequencer is simultaneously collecting off-chain transactions and preparing to send the next batch to the Rollup contract, it could be disrupted by Bob’s forcibly inserted transaction that takes immediate effect. To avoid this issue, Rollups generally do not allow Force Inclusion transactions to take effect immediately. Instead, users initially insert their transactions into a waiting queue on L1, where they enter a “preparation” state. When the sequencer packages off-chain transactions to send to the Rollup contract, it can choose whether to include these queued transactions. If the sequencer continually ignores the transactions in the “preparation” state, once the window period ends, users can forcibly insert these transactions into the Rollup contract. The sequencer can decide when to “incidentally include” transactions from the waiting queue, but it can still refuse to process them. If the sequencer consistently refuses, after a certain period, anyone can use the Force Inclusion function to forcibly insert the transactions into the Rollup contract. Next, we will introduce the implementation of the Force Inclusion mechanism in four prominent Rollups: Optimism, Arbitrum, StarkNet, and zkSync.
Sequencer can choose when to get transactions from the waiting queue.
Sequencers can still refuse to process transactions in the waiting queue.
If the sequencer consistently refuses to process transactions, after a certain period, anyone can use the Force Inclusion function to forcibly insert transactions into the Rollup contract. Next, we will introduce how the Force Inclusion mechanism is implemented in four prominent Rollups: Optimism, Arbitrum, StarkNet, and zkSync.
First, let’s discuss Optimism’s Deposit process. This Deposit process involves not only transferring funds into Optimism but also sending “user messages to L2.” When an L2 node receives a newly deposited message, it converts the message into an L2 transaction and executes it, delivering it to the specified recipient.
User Messages Deposited from L1 to L2
L1CrossDomainMessenger Contract
When a user wants to deposit ETH or ERC-20 tokens into Optimism, they interact with the L1StandardBridge contract on L1 via a frontend webpage, specifying the amount to deposit and the L2 address that will receive these assets. The L1StandardBridge contract then forwards the message to the L1CrossDomainMessenger contract, which acts as the primary communication bridge between L1 and L2. The L1StandardBridge uses this communication component to interact with the L2StandardBridge on L2, determining who can mint tokens on L2 or unlock tokens from L1. Developers who need to create contracts that interoperate and synchronize states between L1 and L2 can build them on top of the L1CrossDomainMessenger contract.
User Messages Transmitted from L1 to L2 via the CrossDomainMessenger Contract
Note: In some images in this article, CrossDomainMessenger is written as CrossChainMessenger.
OptimismPortal Contract
The L1CrossDomainMessenger contract then forwards the message to the lowest layer, the OptimismPortal contract. After processing the message, the OptimismPortal contract emits an event called TransactionDeposited, which includes parameters such as the “sender”, the “receiver”, and other relevant execution details. The Optimism nodes on L2 listen for this TransactionDeposited event from the OptimismPortal contract and convert the event’s parameters into an L2 transaction. The initiator of this transaction will be the “sender” specified in the event, the receiver will be the “receiver” mentioned in the event, and other transaction details will also be derived from the event’s parameters.
L2 nodes convert the parameters of the Transaction Deposited event emitted by OptimismPortal into an L2 transaction.
For example, when a user deposits 0.01 ETH through the L1StandardBridge contract, the message and ETH are transmitted to the OptimismPortal contract (address 0xbEb5…06Ed). A few minutes later, this is converted into an L2 transaction: the message sender is the L1CrossDomainMessenger contract, the receiver is the L2CrossDomainMessenger contract on L2, and the message content indicates that the L1StandardBridge received a 0.01 ETH deposit from Bob. This then triggers additional processes, such as minting 0.01 ETH for the L2StandardBridge, which subsequently transfers it to Bob.
How to Trigger It
If you want to forcibly include a transaction in Optimism’s Rollup contract, your goal is to ensure that a transaction “initiated and executed from your L2 address on L2” can be successfully executed. To achieve this, 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 matches the L1 address format, so you can call this contract using an L1 account with the same address as your L2 account). The “sender” of the L2 transaction derived from the Transaction Deposited event emitted by this contract will then be your L2 account, and the transaction format will be consistent with a standard L2 transaction.
In the L2 transaction derived from the Transaction Deposited event, Bob himself will be the initiator; the receiver will be the Uniswap contract; and it will include the specified ETH, just as if Bob were initiating the L2 transaction himself.
To use Optimism’s Force Inclusion function, you need to directly call the depositTransaction function of the OptimismPortal contract and input the parameters of the transaction you want to execute on L2. I conducted a simple Force Inclusion experiment. The goal of this transaction was to perform a self-transfer on L2 using my address (0xeDc1…6909) and include a message saying “force inclusion.” This is the L1 transaction I executed by calling the depositTransaction function via the OptimismPortal contract. As you can see from the Transaction Deposited event it emitted, both the sender and the receiver are myself.
The remaining values in the opaque Data column encode information such as “how much ETH the person calling the depositTransaction function attached,” “how much ETH the L2 transaction initiator wants to send to the receiver,” “L2 transaction GasLimit,” and “Data for the L2 receiver.” After decoding this information, you will get the following details: “how much ETH the person calling 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 receiver”: 5566 (wei); “L2 transaction GasLimit”: 50000; “Data for the L2 receiver”: 0x666f72636520696e636c7573696f6e, which is the hexadecimal encoding of the string “force inclusion.” Shortly after, the converted L2 transaction appeared: an L2 transaction where I transfer 5566 wei to myself, with the Data field containing the string “force inclusion.” Additionally, in the second-to-last line of the Other Attributes section, the TxnType (transaction type) is shown 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 in the depositTransaction function. Just remember to use the same L1 address as your L2 account when calling the depositTransaction function. This way, when the Deposited Event is converted into an L2 transaction, the initiator will be your L2 account. Sequencer Window The Optimism L2 node that converts the Transaction Deposited event into an L2 transaction is actually the Sequencer. Since this involves transaction ordering, only the Sequencer can decide when to convert the event into an L2 transaction. When the Sequencer listens to the TransactionDeposited event, it does not necessarily convert the event into an L2 transaction immediately; there can be a delay. The maximum duration of this delay is called the Sequencer Window. Currently, the Sequencer Window on the Optimism mainnet is 24 hours. This means that when a user deposits money from L1 or uses Force Inclusion for a transaction, in the worst-case scenario, it will be included in the L2 transaction history after 24 hours.
In Optimism, the L1 deposit operation triggers a Transaction Deposited event, and then it’s just a matter of waiting for the Sequencer to include the operation. However, in Arbitrum, operations on L1 (such as depositing funds or sending messages to L2) are stored in a queue on L1, rather than simply emitting an event. The Sequencer has a specific period to include these queued transactions into the L2 transaction history. If the Sequencer fails to do so within this time frame, anyone can step in to complete the inclusion on behalf of the Sequencer.
Arbitrum maintains a Queue in an L1 contract. If the Sequencer fails to process the transactions in the Queue within a certain period, anyone can forcibly include these transactions into the L2 transaction history. In Arbitrum’s design, operations on L1, such as deposits, must go through the Delayed Inbox contract, where, as the name suggests, these operations will be delayed before taking effect. Another contract, the Sequencer Inbox, is where the Sequencer directly uploads L2 transactions to L1. Each time the Sequencer uploads L2 transactions, it can also take some pending transactions from the Delayed Inbox and include them in the transaction history.
When the Sequencer writes new transactions, it can also include transactions from the DelayedInbox.
Complex Design and Lack of Reference Materials
If you refer to Arbitrum’s official documentation on the Sequencer and Force Inclusion, you’ll find a general explanation of how Force Inclusion works, along with some parameter and function names: Users first call the sendUnsignedTransaction function on the DelayedInbox contract. If the Sequencer doesn’t include it within about 24 hours, users can call the forceInclusion function on the SequencerInbox contract. However, the official documentation doesn’t provide links to these functions, so you have to look them up in the contract code yourself. When you find the sendUnsignedTransaction function, you realize that you have to fill in the nonce value and the maxFeePerGas value yourself. Whose nonce is it? Which network’s maxFeePerGas? How should you fill it in correctly? There are no reference documents, not even NatSpec. You’ll also find many similar 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, not even NatSpec.
You try filling in the parameters and submitting the transaction with a trial-and-error approach, hoping to find the correct usage. However, you discover that all these functions apply Address Aliasing to your L1 address, causing the Sender of the transaction on L2 to be a completely different address, leaving your L2 address inactive. Later, you accidentally stumbled upon a Google search result revealing that Arbitrum has a Tutorial library with scripts demonstrating how to send L2 transactions from L1 (essentially Force Inclusion). The tutorial lists a function not previously mentioned: sendL2Message. Surprisingly, the message parameter required is actually a signed L2 transaction using an L2 account. Who would have known that the “message sent to L2 through Force Inclusion” is actually a “signed L2 transaction”? Moreover, there are no documents or NatSpec explaining when and how to use this function.
Conclusion: Manually generating a forced transaction on Arbitrum is quite complicated. It is recommended to follow the official Tutorial and use the Arbitrum SDK. Unlike other Rollups, Arbitrum lacks clear developer documentation and code annotations. Many functions lack explanations for their purposes and parameters, causing developers to spend much more time than expected to integrate and use them. I also asked for help in the Arbitrum Discord, but did not receive satisfactory answers. When asking on Discord, they only directed me to look at sendL2Message and did not explain the functions of other methods (including those mentioned in the Force Inclusion documentation like sendUnsignedTransaction), their purposes, how to use them, or when to use them.
Unfortunately, StarkNet currently does not have a Force Inclusion mechanism. There are only two articles on the official forum discussing Censorship and Force Inclusion. The reason for the inability to prove failed transactions is that StarkNet’s zero-knowledge proof system cannot prove a failed transaction, so Force Inclusion cannot be allowed. If someone maliciously (or unintentionally) Force Includes a failed, unprovable transaction, StarkNet would get stuck: because once the transaction is forcibly included, the Prover must prove the failed transaction, but it cannot. StarkNet is expected to introduce the ability to prove failed transactions in version v0.15.0, after which the Force Inclusion mechanism should be further implemented.
zkSync’s mechanism for L1->L2 message transmission and Force Inclusion is handled through the requestL2Transaction function of the MailBox contract. Users specify the L2 address, calldata, the amount of ETH to attach, L2GasLimit value, and other details. The requestL2Transaction function combines these parameters into an L2 transaction and places it into the PriorityQueue. When the Sequencer packages transactions and uploads them to L1 (via the commitBatches function), it indicates how many transactions to take from the PriorityQueue to include in the L2 transaction records. In terms of Force Inclusion, zkSync is similar to Optimism, where the initiator’s L2 address (the same as the L1 address) is used to call the relevant functions and fill in the necessary details (callee, calldata, etc.), rather than like Arbitrum, which requires a signed L2 transaction. However, in design, it is similar to Arbitrum, as both maintain a queue on L1, and the Sequencer takes pending transactions directly submitted by users from the Queue and writes them into the transaction history.
If you deposit ETH through zkSync’s official bridge, like this transaction, it calls the requestL2Transaction function of the MailBox contract. This function places the Deposit ETH L2 transaction into the PriorityQueue and emits a NewPriorityRequest event. Since the contract encodes the L2 transaction data into a string of bytes, 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). After some time, when the Sequencer takes this L2 transaction out of the PriorityQueue and includes it in the transaction history, it will be converted into an L2 transaction where you transfer to yourself. The transfer amount will be the ETH amount attached by the initiator in the L1 Deposit ETH transaction.
In the L1 Deposit transaction, both the initiator and the recipient are 0xeDc1…6909, the amount is 0.03 ETH, and there is no calldata.
On L2, there will be a transaction where 0xeDc1…6909 transfers to itself. The transaction type (TxnType) is 255, indicating a system transaction. Then, just like I experimented with the forced transaction function on Optimism before, I called zkSync’s requestL2Transaction function and initiated a self-transfer transaction: no ETH was attached, and the calldata contained the HEX encoding of the string “force inclusion.” This was then converted into an L2 transaction where I transfer to myself, with the calldata containing the hexadecimal string for “force inclusion”: 0x666f72636520696e636c7573696f6e.
When the Sequencer takes transactions from the PriorityQueue and writes them into the transaction history, they are converted into corresponding L2 transactions. Using the requestL2Transaction function, users can submit data on L1 with the same L1 account as their L2 address, specifying the L2 recipient, the amount of ETH to attach, and the calldata. If users want to call other contracts or include different Data, they simply need to fill in the parameters in the requestL2Transaction function.
No Force Inclusion Function for Users Yet
Although an L2 transaction placed in the PriorityQueue will have a calculated waiting period for inclusion by the Sequencer, zkSync’s current design does not have a Force Inclusion function that allows users to enforce it. This means it is only a partial solution. Even though there is a “waiting period for inclusion,” it ultimately depends on whether the Sequencer decides to include it: the Sequencer can include it after the period expires or never include any transactions from the PriorityQueue. In the future, zkSync should add functions that allow users to forcibly include transactions into the L2 transaction history if they have not been included by the Sequencer after the waiting period. This would be a truly effective Force Inclusion mechanism.
L1 relies on a large number of validators to ensure the network’s “security” and “censorship resistance.” Rollups, however, have weaker censorship resistance because transactions are written by a few or even a single Sequencer. Therefore, Rollups need a Force Inclusion mechanism to allow users to bypass the Sequencer and write transactions into the history, preventing censorship by the Sequencer from making the Rollup unusable and preventing users from withdrawing funds. Force Inclusion allows users to forcibly write transactions into the history, but the design must choose whether “transactions can be immediately inserted into the history and take effect immediately.” If immediate effect is allowed, it would negatively impact the Sequencer because pending transactions on L2 could be affected by forcibly included transactions from L1. Therefore, the current Force Inclusion mechanisms in Rollups first place the transactions inserted from L1 into a waiting state and give the Sequencer a time window to react and decide whether to include these pending transactions. zkSync and Arbitrum both maintain a queue on L1 to manage L2 transactions or messages sent from L1 to L2. Arbitrum calls it DelayedInbox; zkSync calls it PriorityQueue. However, zkSync’s method of sending L2 transactions is more similar to Optimism, where messages are sent from L1 using the L2 address, so that when converted to an L2 transaction, the initiator is the 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 L2 transaction and signs it, then sends it through the sendL2Message function. On L2, Arbitrum uses the signature to restore the signer as the initiator of the L2 transaction. StarkNet currently does not have a Force Inclusion mechanism; zkSync has a half-implemented Force Inclusion mechanism—it has a PriorityQueue, and each L2 transaction in the Queue has an inclusion validity period, but this validity period is currently just for show. In practice, the Sequencer can choose not to include any L2 transactions from the PriorityQueue.
This article is forwarded from: [Geek Web3], the original title is “Theory and Practice: How to trigger censorship-resistant transactions in Ethereum Rollup?”, copyright attribution to original author [NIC Lin, Head of Taipei Ethereum Meetup], if you have any objection to the reprint, please contact Gate Learn Team, the team will handle it as soon as possible according to relevant procedures.
Disclaimer: The views and opinions expressed in this article represent only the author’s personal views and do not constitute any investment advice.
Other language versions of the article are translated by the Gate Learn team. Without referencing Gate.io, copying, distributing, or plagiarizing the translated articles is prohibited.
Just yesterday, a shocking event occurred: Linea, the Ethereum Layer 2 solution developed by Consensys, the parent company of Metamask, was proactively shut down. The official reason given was to mitigate the impact of the Velocore hacking incident. This incident inevitably brings to mind a previous case where the BSC chain (BNB Chain) was also shut down under official coordination to minimize hacking losses. These events often lead people to question the decentralized values that Web3 advocates.
The core reason behind the aforementioned events lies in the imperfection of the infrastructure, specifically its lack of decentralization. If a blockchain were sufficiently decentralized, it shouldn’t be able to shut down so easily. Due to the unique structure of Ethereum Layer 2, most Layer 2 solutions rely on centralized sequencers. Despite the growing discourse on decentralized sequencers in recent years, given the purpose and structure of Layer 2, we can assume that Layer 2 sequencers are unlikely to achieve high levels of decentralization. In fact, they may end up being less decentralized than the BSC chain. If this is the case, what can be done? For Layer 2, the most immediate risks of non-decentralized sequencers are a lack of censorship resistance and liveness. If there are only a few entities processing transactions (sequencers), they have absolute power over whether to serve you or not: they can refuse your transactions at will, leaving you without recourse. Addressing the censorship resistance issue in Layer 2 is clearly an important topic. Over the past few years, various Ethereum Layer 2 solutions have proposed different approaches to tackle this issue. For instance, Loopring, Degate, and StarkEx have introduced forced withdrawal and escape hatch functions, while Arbitrum and other Optimistic Rollups have implemented Force Inclusion features. These mechanisms can impose checks on the sequencers to prevent arbitrary refusal of user transactions. In today’s article, NIC Lin from the Taipei Ethereum Association shares his firsthand experience, experimenting with the censorship-resistant transaction features of four major Rollups and providing an in-depth analysis of the Force Inclusion mechanism, focusing on workflow and operational methods. This analysis is especially valuable for the Ethereum community and large asset holders.
Censorship resistance in transactions is crucial for any blockchain. If a blockchain can arbitrarily censor and reject user transactions, it is no different from a Web2 server. Ethereum’s transaction censorship resistance is currently ensured by its numerous validators. If someone wants to censor Bob’s transaction and prevent it from being included in the blockchain, they would either have to bribe the majority of the network’s validators or spam the network with garbage transactions that have higher fees than Bob’s, thus occupying block space. Both methods are extremely costly.
Note: In Ethereum’s current Proposer-Builder Separation (PBS) architecture, the cost of censoring transactions is significantly reduced. For example, you can look at the proportion of blocks that comply with OFAC’s censorship of Tornado Cash transactions. The current censorship resistance relies on independent validators and relays that are outside the jurisdiction of OFAC and other government entities.
But what about Rollups? Rollups do not require a large number of validators to ensure security. Even if a Rollup has only one centralized entity (Sequencer) producing blocks, it remains as secure as Layer 1 (L1). However, security and censorship resistance are two different matters. A Rollup, while being as secure as Ethereum, can still censor any user’s transaction if it only has a single centralized sequencer.
Sequencer can refuse to process a user’s transaction, resulting in the user’s funds being locked and unable to leave the Rollup.
Instead of requiring Rollups to have a large number of decentralized sequencers, it is more effective to directly leverage the censorship resistance of Layer 1 (L1):
Since the sequencer needs to package transaction data and send it to the Rollup contract on L1, we can add a feature in the contract that allows users to insert their transactions into the Rollup contract themselves. This mechanism is known as “Force Inclusion.” As long as the sequencer cannot censor users at the L1 level, it cannot prevent users from forcibly inserting transactions at L1. This way, the Rollup can inherit the censorship resistance of L1.
Sequencer cannot review the user’s L1 transactions without paying a high cost
If transactions are allowed to be directly written into the Rollup contract through Force Inclusion (meaning they take effect immediately), the state of the Rollup will change instantly. For example, if Bob uses the Force Inclusion mechanism to insert a transaction that transfers 1000 DAI to Carol, and the transaction takes effect immediately, Bob’s balance would decrease by 1000 DAI, while Carol’s balance would increase by 1000 DAI in the updated state.
If Force Inclusion allows transactions to be directly written into the Rollup contract and take effect immediately, the state of the Rollup would change instantly. If the sequencer is simultaneously collecting off-chain transactions and preparing to send the next batch to the Rollup contract, it could be disrupted by Bob’s forcibly inserted transaction that takes immediate effect. To avoid this issue, Rollups generally do not allow Force Inclusion transactions to take effect immediately. Instead, users initially insert their transactions into a waiting queue on L1, where they enter a “preparation” state. When the sequencer packages off-chain transactions to send to the Rollup contract, it can choose whether to include these queued transactions. If the sequencer continually ignores the transactions in the “preparation” state, once the window period ends, users can forcibly insert these transactions into the Rollup contract. The sequencer can decide when to “incidentally include” transactions from the waiting queue, but it can still refuse to process them. If the sequencer consistently refuses, after a certain period, anyone can use the Force Inclusion function to forcibly insert the transactions into the Rollup contract. Next, we will introduce the implementation of the Force Inclusion mechanism in four prominent Rollups: Optimism, Arbitrum, StarkNet, and zkSync.
Sequencer can choose when to get transactions from the waiting queue.
Sequencers can still refuse to process transactions in the waiting queue.
If the sequencer consistently refuses to process transactions, after a certain period, anyone can use the Force Inclusion function to forcibly insert transactions into the Rollup contract. Next, we will introduce how the Force Inclusion mechanism is implemented in four prominent Rollups: Optimism, Arbitrum, StarkNet, and zkSync.
First, let’s discuss Optimism’s Deposit process. This Deposit process involves not only transferring funds into Optimism but also sending “user messages to L2.” When an L2 node receives a newly deposited message, it converts the message into an L2 transaction and executes it, delivering it to the specified recipient.
User Messages Deposited from L1 to L2
L1CrossDomainMessenger Contract
When a user wants to deposit ETH or ERC-20 tokens into Optimism, they interact with the L1StandardBridge contract on L1 via a frontend webpage, specifying the amount to deposit and the L2 address that will receive these assets. The L1StandardBridge contract then forwards the message to the L1CrossDomainMessenger contract, which acts as the primary communication bridge between L1 and L2. The L1StandardBridge uses this communication component to interact with the L2StandardBridge on L2, determining who can mint tokens on L2 or unlock tokens from L1. Developers who need to create contracts that interoperate and synchronize states between L1 and L2 can build them on top of the L1CrossDomainMessenger contract.
User Messages Transmitted from L1 to L2 via the CrossDomainMessenger Contract
Note: In some images in this article, CrossDomainMessenger is written as CrossChainMessenger.
OptimismPortal Contract
The L1CrossDomainMessenger contract then forwards the message to the lowest layer, the OptimismPortal contract. After processing the message, the OptimismPortal contract emits an event called TransactionDeposited, which includes parameters such as the “sender”, the “receiver”, and other relevant execution details. The Optimism nodes on L2 listen for this TransactionDeposited event from the OptimismPortal contract and convert the event’s parameters into an L2 transaction. The initiator of this transaction will be the “sender” specified in the event, the receiver will be the “receiver” mentioned in the event, and other transaction details will also be derived from the event’s parameters.
L2 nodes convert the parameters of the Transaction Deposited event emitted by OptimismPortal into an L2 transaction.
For example, when a user deposits 0.01 ETH through the L1StandardBridge contract, the message and ETH are transmitted to the OptimismPortal contract (address 0xbEb5…06Ed). A few minutes later, this is converted into an L2 transaction: the message sender is the L1CrossDomainMessenger contract, the receiver is the L2CrossDomainMessenger contract on L2, and the message content indicates that the L1StandardBridge received a 0.01 ETH deposit from Bob. This then triggers additional processes, such as minting 0.01 ETH for the L2StandardBridge, which subsequently transfers it to Bob.
How to Trigger It
If you want to forcibly include a transaction in Optimism’s Rollup contract, your goal is to ensure that a transaction “initiated and executed from your L2 address on L2” can be successfully executed. To achieve this, 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 matches the L1 address format, so you can call this contract using an L1 account with the same address as your L2 account). The “sender” of the L2 transaction derived from the Transaction Deposited event emitted by this contract will then be your L2 account, and the transaction format will be consistent with a standard L2 transaction.
In the L2 transaction derived from the Transaction Deposited event, Bob himself will be the initiator; the receiver will be the Uniswap contract; and it will include the specified ETH, just as if Bob were initiating the L2 transaction himself.
To use Optimism’s Force Inclusion function, you need to directly call the depositTransaction function of the OptimismPortal contract and input the parameters of the transaction you want to execute on L2. I conducted a simple Force Inclusion experiment. The goal of this transaction was to perform a self-transfer on L2 using my address (0xeDc1…6909) and include a message saying “force inclusion.” This is the L1 transaction I executed by calling the depositTransaction function via the OptimismPortal contract. As you can see from the Transaction Deposited event it emitted, both the sender and the receiver are myself.
The remaining values in the opaque Data column encode information such as “how much ETH the person calling the depositTransaction function attached,” “how much ETH the L2 transaction initiator wants to send to the receiver,” “L2 transaction GasLimit,” and “Data for the L2 receiver.” After decoding this information, you will get the following details: “how much ETH the person calling 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 receiver”: 5566 (wei); “L2 transaction GasLimit”: 50000; “Data for the L2 receiver”: 0x666f72636520696e636c7573696f6e, which is the hexadecimal encoding of the string “force inclusion.” Shortly after, the converted L2 transaction appeared: an L2 transaction where I transfer 5566 wei to myself, with the Data field containing the string “force inclusion.” Additionally, in the second-to-last line of the Other Attributes section, the TxnType (transaction type) is shown 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 in the depositTransaction function. Just remember to use the same L1 address as your L2 account when calling the depositTransaction function. This way, when the Deposited Event is converted into an L2 transaction, the initiator will be your L2 account. Sequencer Window The Optimism L2 node that converts the Transaction Deposited event into an L2 transaction is actually the Sequencer. Since this involves transaction ordering, only the Sequencer can decide when to convert the event into an L2 transaction. When the Sequencer listens to the TransactionDeposited event, it does not necessarily convert the event into an L2 transaction immediately; there can be a delay. The maximum duration of this delay is called the Sequencer Window. Currently, the Sequencer Window on the Optimism mainnet is 24 hours. This means that when a user deposits money from L1 or uses Force Inclusion for a transaction, in the worst-case scenario, it will be included in the L2 transaction history after 24 hours.
In Optimism, the L1 deposit operation triggers a Transaction Deposited event, and then it’s just a matter of waiting for the Sequencer to include the operation. However, in Arbitrum, operations on L1 (such as depositing funds or sending messages to L2) are stored in a queue on L1, rather than simply emitting an event. The Sequencer has a specific period to include these queued transactions into the L2 transaction history. If the Sequencer fails to do so within this time frame, anyone can step in to complete the inclusion on behalf of the Sequencer.
Arbitrum maintains a Queue in an L1 contract. If the Sequencer fails to process the transactions in the Queue within a certain period, anyone can forcibly include these transactions into the L2 transaction history. In Arbitrum’s design, operations on L1, such as deposits, must go through the Delayed Inbox contract, where, as the name suggests, these operations will be delayed before taking effect. Another contract, the Sequencer Inbox, is where the Sequencer directly uploads L2 transactions to L1. Each time the Sequencer uploads L2 transactions, it can also take some pending transactions from the Delayed Inbox and include them in the transaction history.
When the Sequencer writes new transactions, it can also include transactions from the DelayedInbox.
Complex Design and Lack of Reference Materials
If you refer to Arbitrum’s official documentation on the Sequencer and Force Inclusion, you’ll find a general explanation of how Force Inclusion works, along with some parameter and function names: Users first call the sendUnsignedTransaction function on the DelayedInbox contract. If the Sequencer doesn’t include it within about 24 hours, users can call the forceInclusion function on the SequencerInbox contract. However, the official documentation doesn’t provide links to these functions, so you have to look them up in the contract code yourself. When you find the sendUnsignedTransaction function, you realize that you have to fill in the nonce value and the maxFeePerGas value yourself. Whose nonce is it? Which network’s maxFeePerGas? How should you fill it in correctly? There are no reference documents, not even NatSpec. You’ll also find many similar 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, not even NatSpec.
You try filling in the parameters and submitting the transaction with a trial-and-error approach, hoping to find the correct usage. However, you discover that all these functions apply Address Aliasing to your L1 address, causing the Sender of the transaction on L2 to be a completely different address, leaving your L2 address inactive. Later, you accidentally stumbled upon a Google search result revealing that Arbitrum has a Tutorial library with scripts demonstrating how to send L2 transactions from L1 (essentially Force Inclusion). The tutorial lists a function not previously mentioned: sendL2Message. Surprisingly, the message parameter required is actually a signed L2 transaction using an L2 account. Who would have known that the “message sent to L2 through Force Inclusion” is actually a “signed L2 transaction”? Moreover, there are no documents or NatSpec explaining when and how to use this function.
Conclusion: Manually generating a forced transaction on Arbitrum is quite complicated. It is recommended to follow the official Tutorial and use the Arbitrum SDK. Unlike other Rollups, Arbitrum lacks clear developer documentation and code annotations. Many functions lack explanations for their purposes and parameters, causing developers to spend much more time than expected to integrate and use them. I also asked for help in the Arbitrum Discord, but did not receive satisfactory answers. When asking on Discord, they only directed me to look at sendL2Message and did not explain the functions of other methods (including those mentioned in the Force Inclusion documentation like sendUnsignedTransaction), their purposes, how to use them, or when to use them.
Unfortunately, StarkNet currently does not have a Force Inclusion mechanism. There are only two articles on the official forum discussing Censorship and Force Inclusion. The reason for the inability to prove failed transactions is that StarkNet’s zero-knowledge proof system cannot prove a failed transaction, so Force Inclusion cannot be allowed. If someone maliciously (or unintentionally) Force Includes a failed, unprovable transaction, StarkNet would get stuck: because once the transaction is forcibly included, the Prover must prove the failed transaction, but it cannot. StarkNet is expected to introduce the ability to prove failed transactions in version v0.15.0, after which the Force Inclusion mechanism should be further implemented.
zkSync’s mechanism for L1->L2 message transmission and Force Inclusion is handled through the requestL2Transaction function of the MailBox contract. Users specify the L2 address, calldata, the amount of ETH to attach, L2GasLimit value, and other details. The requestL2Transaction function combines these parameters into an L2 transaction and places it into the PriorityQueue. When the Sequencer packages transactions and uploads them to L1 (via the commitBatches function), it indicates how many transactions to take from the PriorityQueue to include in the L2 transaction records. In terms of Force Inclusion, zkSync is similar to Optimism, where the initiator’s L2 address (the same as the L1 address) is used to call the relevant functions and fill in the necessary details (callee, calldata, etc.), rather than like Arbitrum, which requires a signed L2 transaction. However, in design, it is similar to Arbitrum, as both maintain a queue on L1, and the Sequencer takes pending transactions directly submitted by users from the Queue and writes them into the transaction history.
If you deposit ETH through zkSync’s official bridge, like this transaction, it calls the requestL2Transaction function of the MailBox contract. This function places the Deposit ETH L2 transaction into the PriorityQueue and emits a NewPriorityRequest event. Since the contract encodes the L2 transaction data into a string of bytes, 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). After some time, when the Sequencer takes this L2 transaction out of the PriorityQueue and includes it in the transaction history, it will be converted into an L2 transaction where you transfer to yourself. The transfer amount will be the ETH amount attached by the initiator in the L1 Deposit ETH transaction.
In the L1 Deposit transaction, both the initiator and the recipient are 0xeDc1…6909, the amount is 0.03 ETH, and there is no calldata.
On L2, there will be a transaction where 0xeDc1…6909 transfers to itself. The transaction type (TxnType) is 255, indicating a system transaction. Then, just like I experimented with the forced transaction function on Optimism before, I called zkSync’s requestL2Transaction function and initiated a self-transfer transaction: no ETH was attached, and the calldata contained the HEX encoding of the string “force inclusion.” This was then converted into an L2 transaction where I transfer to myself, with the calldata containing the hexadecimal string for “force inclusion”: 0x666f72636520696e636c7573696f6e.
When the Sequencer takes transactions from the PriorityQueue and writes them into the transaction history, they are converted into corresponding L2 transactions. Using the requestL2Transaction function, users can submit data on L1 with the same L1 account as their L2 address, specifying the L2 recipient, the amount of ETH to attach, and the calldata. If users want to call other contracts or include different Data, they simply need to fill in the parameters in the requestL2Transaction function.
No Force Inclusion Function for Users Yet
Although an L2 transaction placed in the PriorityQueue will have a calculated waiting period for inclusion by the Sequencer, zkSync’s current design does not have a Force Inclusion function that allows users to enforce it. This means it is only a partial solution. Even though there is a “waiting period for inclusion,” it ultimately depends on whether the Sequencer decides to include it: the Sequencer can include it after the period expires or never include any transactions from the PriorityQueue. In the future, zkSync should add functions that allow users to forcibly include transactions into the L2 transaction history if they have not been included by the Sequencer after the waiting period. This would be a truly effective Force Inclusion mechanism.
L1 relies on a large number of validators to ensure the network’s “security” and “censorship resistance.” Rollups, however, have weaker censorship resistance because transactions are written by a few or even a single Sequencer. Therefore, Rollups need a Force Inclusion mechanism to allow users to bypass the Sequencer and write transactions into the history, preventing censorship by the Sequencer from making the Rollup unusable and preventing users from withdrawing funds. Force Inclusion allows users to forcibly write transactions into the history, but the design must choose whether “transactions can be immediately inserted into the history and take effect immediately.” If immediate effect is allowed, it would negatively impact the Sequencer because pending transactions on L2 could be affected by forcibly included transactions from L1. Therefore, the current Force Inclusion mechanisms in Rollups first place the transactions inserted from L1 into a waiting state and give the Sequencer a time window to react and decide whether to include these pending transactions. zkSync and Arbitrum both maintain a queue on L1 to manage L2 transactions or messages sent from L1 to L2. Arbitrum calls it DelayedInbox; zkSync calls it PriorityQueue. However, zkSync’s method of sending L2 transactions is more similar to Optimism, where messages are sent from L1 using the L2 address, so that when converted to an L2 transaction, the initiator is the 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 L2 transaction and signs it, then sends it through the sendL2Message function. On L2, Arbitrum uses the signature to restore the signer as the initiator of the L2 transaction. StarkNet currently does not have a Force Inclusion mechanism; zkSync has a half-implemented Force Inclusion mechanism—it has a PriorityQueue, and each L2 transaction in the Queue has an inclusion validity period, but this validity period is currently just for show. In practice, the Sequencer can choose not to include any L2 transactions from the PriorityQueue.
This article is forwarded from: [Geek Web3], the original title is “Theory and Practice: How to trigger censorship-resistant transactions in Ethereum Rollup?”, copyright attribution to original author [NIC Lin, Head of Taipei Ethereum Meetup], if you have any objection to the reprint, please contact Gate Learn Team, the team will handle it as soon as possible according to relevant procedures.
Disclaimer: The views and opinions expressed in this article represent only the author’s personal views and do not constitute any investment advice.
Other language versions of the article are translated by the Gate Learn team. Without referencing Gate.io, copying, distributing, or plagiarizing the translated articles is prohibited.