Original title: "Introduction to Rollup's Force Inclusion Mechanism"

Author: NIC Lin, Taipei Ethereum Meetup Leader

 

Just yesterday, something happened that shocked countless people: Linea, the second layer of Ethereum launched by Metamask's parent company Consensys, took the initiative to shut down. The official said that the purpose of doing so was to reduce the impact of the Velocore hacker attack. This can't help but remind people of the previous BSC chain (BNB Chain) shut down under the official coordination to reduce the losses of hacker attacks. Whenever people talk about this kind of thing, they will doubt the decentralized value advocated by Web3.

Of course, the core reason for the above incidents lies more in the imperfection of the infrastructure itself, that is, the lack of decentralization: if a chain is decentralized enough, it should not be stopped at will. Due to the unique structure of Ethereum Layer 2, most Layer2s rely on centralized sequencers. Although there have been more and more arguments for decentralized sequencers in recent years, considering the purpose of the second layer and its structure, we can assume that the Layer2 sequencer is unlikely to be very decentralized, and may not even be as decentralized as the BSC chain in the end. If this is true, what should we do?

In fact, for the second layer, the most direct harm caused by the lack of decentralization of the sequencer lies in anti-censorship and activity. If there are few entities (Sequencers) processing transactions, then it has absolute power over whether to serve you: it can reject you if it wants to, and you may have no choice. How to solve the anti-censorship problem of Layer2 is obviously an important topic.

In the past few years, major Ethereum Layer 2 projects have proposed various solutions to the problem of anti-censorship, such as Loopring and Degate, StarkEx's forced withdrawal and escape pod functions, Arbitrum and other OP Rollup's Force Inclusion functions. These methods can, under certain conditions, check and balance the Sequencer to prevent it from arbitrarily rejecting any user's transaction requests.

In today’s article, NIC Lin from the Taipei Ethereum Association shared his personal experience and experimented with the anti-censorship transaction functions of four mainstream Rollups. He conducted an in-depth analysis of the mechanism design of Force Inclusion from aspects such as workflow and operation methods. This is especially valuable for the Ethereum community and large investors with huge assets.

Transaction Review and Force Inclusion

Censorship resistance is very important for a blockchain. If a blockchain can arbitrarily censor and reject transactions initiated by users, it is no different from a Web2 server. Ethereum's current transaction censorship resistance comes from its large number of validators. If someone wants to censor Bob's transactions and prevent his transactions from being uploaded to the chain, they can either try to buy off most of the validators in the network or spam the entire network and continuously send out junk transactions with higher fees than Bob to grab block space. Either way, the cost will be very high.

Note: In Ethereum’s current PBS architecture, the cost of reviewing transactions will be significantly reduced. You can refer to the block ratio of Tornado Cash transactions that cooperate with OFAC to review. The current anti-censorship capabilities rely on independent validators and relays outside the jurisdiction of OFAC and the government.

But what about Rollup? Rollup does not need a large number of validators to ensure security. Even if Rollup has only one centralized role (Sequencer) to produce blocks, it is as secure as L1. But security and anti-censorship are two different things. Even if a Rollup is as secure as Ethereum, if there is only one centralized Sequencer, any user's transaction can be censored.

The Sequencer can refuse to process the user's transaction, causing the user's funds to be withheld and unable to leave the Rollup

Force Inclusion Mechanism

Instead of requiring Rollup to have a large number of decentralized sequencers, it is better to directly utilize L1’s anti-censorship capabilities:

Originally, Sequencer was to package transaction data and send it to the Rollup contract of L1. It is better to add a design in the contract to allow users to insert transactions into the Rollup contract by themselves. This mechanism is called "Force Inclusion". As long as Sequencer cannot censor users at the L1 level, it cannot prevent users from forcibly inserting transactions in L1. In this way, Rollup can inherit the anti-censorship ability of L1.

Sequencer cannot censor user’s L1 transactions without paying a high cost

How should forced transactions be effective?

If Force Inclusion is allowed to write transactions directly into the Rollup contract (that is, take effect immediately), the state of the Rollup will change immediately. For example, Bob inserts a transaction of "transfer 1,000 DAI to Carol" through the Force Inclusion mechanism. If the transaction takes effect immediately, then in the latest state, Bob's balance will be 1,000 DAI less and Carol will have 1,000 DAI more.

If Force Inclusion can write the transaction directly into the Rollup contract and take effect immediately, the status will change immediately.

If the Sequencer is also collecting transactions off-chain at this time and sending the next batch of transactions to the Rollup contract, it may be affected by the transaction that Bob forcefully inserts and takes effect immediately. This problem should be avoided as much as possible, so Rollup generally does not allow Force Inclusion transactions to take effect immediately, but first allows users to insert transactions into the waiting queue on L1 and enter the "preparing" state.

When the Sequencer packages off-chain transactions and sends them to the Rollup contract, it chooses whether to insert the aforementioned transactions into the transaction sequence. If the Sequencer always ignores these transactions in the "preparation" state, after the window period ends, the user can force these transactions to be inserted into the Rollup contract.

The Sequencer can decide when to "collect" transactions waiting in the queue

The Sequencer can still refuse to process transactions in the waiting queue.

If the Sequencer refuses for a long time, after a period of time, anyone can force the transaction into the Rollup contract through the Force Inclusion function.

Next, we will introduce the Force Inclusion mechanism implementations of four more famous Rollups, namely 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 the information sent by the user to L2" into L2. After receiving the newly deposited message, the L2 node will convert the message into an L2 transaction for execution and send it to the recipient specified by the message.

User's message from L1 Deposit to L2

L1CrossDomainMessenger Contract

When a user wants to deposit ETH or ERC-20 tokens into Optimism, he interacts with the L1StandardBridge contract on L1 through the front-end web page, specifying the amount to deposit and which L2 address will receive these assets.

The L1StandardBridge contract will pass the message to the L1CrossDomainMessenger contract on the next layer. This contract is mainly used as a component for communication between L1 and L2. L1StandardBridge communicates with L2StandardBridge on L2 through this common communication component to decide who can mint tokens on L2 or who can unlock tokens from L1.

If a developer needs to develop a contract that communicates and synchronizes states between L1 and L2, he can build it on the L1CrossDomainMessenger contract.

The user's message is passed from L1 to L2 via the CrossDomainMessenger contract

Note: In some pictures in this article, CrossDomainMessager is written as CrossChainMessager

OptimismPortal Contract

The L1CrossDomainMessenger contract will then send the message to the bottom-level OptimismPortal contract. After processing, the OptimismPortal contract will throw an event called TransactionDeposited, whose parameters include "sender", "receiver", and related execution parameters.

Then the L2 Optimism node will listen to the Transaction Deposited event thrown by the OptimismPortal contract and convert the parameters in the event into an L2 transaction. The initiator of this transaction will be the "message sender" specified in the Transaction Deposited event parameter. The transaction receiver is the "person who receives the message" in the event parameters, and other transaction parameters are also derived from the parameters in the above events.

The L2 node converts the Transaction Deposited event parameters of OptimismPortalemit 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 transmitted all the way to the OptimismPortal contract (address is 0xbEb5…06Ed), and then converted into an L2 transaction a few minutes later:

The message initiator is the L1CrossDomainMessenger contract; the receiver is the L2CrossDomainMessenger contract on L2; the message content is that L1StandardBridge received BoB's 0.01ETH deposit. After that, some processes will be triggered, such as issuing 0.01 ETH to L2StandardBridge, which will then transfer it to Bob.

How to trigger

When you want to force a transaction to be included in Optimism's Rollup contract, the effect you want to achieve is to allow a "transaction initiated and executed on L2 from your L2 address" to be executed smoothly. At this time, you should use your own L2 address to submit the message directly to the OptimismPortal contract (note that the OptimismPortal contract is actually on L1, but the address format of OP is consistent with the L1 address format. You can directly call the above contract with the L1 account with the same address as the L2 account).

After that, the "initiator" of the L2 transaction converted by the Transaction Deposited event thrown by the contract will be your L2 account, and the transaction format is consistent with the normal L2 transaction.

In the L2 transaction converted from the Transaction Deposited event, the initiator will be Bob himself; the recipient will be the Uniswap contract; and the specified ETH will be attached, just like Bob initiated the L2 transaction himself

If you want to call Optimism's Force Inclusion function, you need to directly call the depositTransaction function of the OptimismPortal contract and fill in the parameters of the transaction you want to execute on L2

I did a simple Force Inclusion experiment. This transaction wanted to achieve one thing: self-transfer using my address on L2 (0xeDc1…6909) with a text message of “force inclusion”.

This is the L1 transaction where I execute the depositTransaction function through the OptimismPortal contract. You can see that in the Transaction Deposited event it throws, both from and to are me.

The values ​​in the remaining opaque Data column encode information such as "how much ETH the person calling the deposit Transaction function attached", "how much ETH the L2 transaction initiator wants to send to the recipient", "L2 transaction GasLimit" and "Data for the L2 recipient".

After decoding the above information, we will get:

“How much ETH is attached by the person calling deposit Transaction”: 0, because I am not depositing ETH from L1 to L2;

“How much ETH does the L2 transaction initiator need to send to the recipient”: 5566 (wei)

"GasLimit for L2 transactions": 50000

"Data for L2 receiver": 0x666f72636520696e636c7573696f6e, which is the hexadecimal encoding of the string "force inclusion"

Soon after, the converted L2 transaction appeared: an L2 transaction where I transferred money to myself, the amount was 5566 wei, and the Data was the "force inclusion" string. And you can notice that in the second-to-last row of Other Attributes, the TxnType (transaction type) is displayed as system transaction 126 (System), indicating that this transaction was not initiated by me in L2, but was converted from the Deposited event of the L1 transaction.

Converted L2 transactions

If you want to call the L2 contract through Force Inclusion and send different data, you just need to fill in the parameters one by one into the previous deposit Transaction function. Just remember to call the deposit Transaction function with the same L1 address as your L2 account. In this way, when the Deposited Event is converted into an L2 transaction, the initiator is your L2 account.

SequencerWindow

The Optimism L2 node mentioned above converts the Transaction Deposited event into an L2 transaction. In fact, this Optimism node refers to the Sequencer. After all, this is related to transaction ordering, so only the Sequencer can decide when to convert the aforementioned event into an L2 transaction.

When monitoring the TransactionDeposited event, the Sequencer does not necessarily convert the event into an L2 transaction immediately. There may be a delay, and the maximum value of this period is called the SequencerWindow.

Currently, the Sequencer Window on the Optimism mainnet is 24 hours, which means that when a user deposits money from L1 or forces inclusion of a transaction, the worst case scenario is that it will not be included in the L2 transaction history until 24 hours later.

Arbitrum’s Force Inclusion Mechanism

In Optimism, the Deposit operation on L1 will throw a Transaction Deposited event, and all that remains is to wait for the Sequencer to record the above operation; but in Arbitrum, operations that occur on L1 (depositing money or sending messages to L2, etc.) will be stored in a queue on L1 instead of simply throwing an event.

The Sequencer will be given a period of time to include the transactions in the above queue into the L2 transaction history. If the Sequencer does nothing when the time comes, anyone can complete it for the Sequencer.

Arbitrum will maintain a Queue in the L1 contract. If the Sequencer does not actively process the transactions in the Queue, anyone can force the transactions in the Queue to be included in the L2 transaction history when the time is up.

In Arbitrum’s design, operations such as deposits on L1 must go through the Delayed Inbox contract. As the name implies, operations here will be delayed in taking effect. Another contract is the Sequencer Inbox, which is the direct place for the Sequencer to upload L2 transactions to L1. Every time the Sequencer uploads an L2 transaction, it can also take out some pending transactions from the Delayed Inbox and write them into the transaction history.

When Sequencer writes a new transaction, it can also take out the transaction from DelayedInbox and write it together

Complex design and excellent references

If readers directly refer to Arbitrum's official chapter on Sequencer and Force Inclusion, they will see a general description of how Force Inclusion works, as well as some parameter names and function names:

The user first calls the sendUnsignedTransaction function in the DelayedInbox contract. If the Sequencer is not included within 24 hours, the user can call the forceInclusion function in the SequencerInbox contract. Arbitrum officials did not attach the function link to the official website document, so you can only look at the corresponding function in the contract code yourself.

When you find the sendUnsignedTransaction function, you find that you have to fill in the nonce value and maxFeePerGas value yourself. Which address's nonce is it? Which network's maxFeePerGas is it? How should I fill it in? There is no document reference, not even Natpsec. Then you will find a bunch of similar-looking functions in the Arbitrum contract:

sendL1FundedUnsignedTransaction, sendUnsignedTransactionToFork, sendContractTransaction, sendL1FundedContractTransaction, there is no document telling you the difference between these functions, how to use them, how to fill in the parameters, not even Natpsec.

You try to fill in the parameters and send the transaction with the mentality of giving it a try. You want to use trial and error to see if you can find the correct usage, but you find that these functions all perform AddressAliasing on your L1 address, resulting in the final error on L2. The Sender when initiating the transaction is simply a different address, so your L2 address remains unchanged.

sendL2Message

Later, I accidentally clicked on Google search and found out that Arbitrum has a Tutorial library, which contains scripts demonstrating how to send L2 transactions from L1 (which is what Force Inclusion means), and the functions listed in it are not any of the functions mentioned above. , but a function called sendL2Message, and the message parameter is actually a transaction signed with the L2 account?

Who would have known that the "message to L2 via Force Inclusion" would actually be a "signed L2 transaction"? And there is no document or Natspec explaining when and how to use this function.

Conclusion: It is troublesome to manually generate an Arbitrum forced transaction. It is recommended to follow the official Tutorial and run the Arbitrum SDK. Unlike other Rollups, Arbitrum has clear developer documentation and code notes. The purpose and parameters of many functions lack explanation, causing developers to spend more time than expected to access and use it. I also asked the people at Arbitrum on the Arbitrum Discord but didn't get a satisfactory answer.

When I asked on Discord, the other party only told me to look at sendL2Message, and did not want to explain the functions of other functions (even sendUnsignedTransaction mentioned in the Force Inclusion document) what they are used for, how to use them, and when to use them.

StarkNet's ForceInclusion mechanism

Unfortunately, StarkNet does not have a ForceInclusion mechanism yet. There are only two posts on the official forums that discuss Censorship and ForceInclusion.

Unable to prove failed transactions

The above reason is actually because StarkNet's zero-knowledge proof system cannot prove a failed transaction, so Force Inclusion cannot be allowed. Because if someone maliciously (or unintentionally) Force Includes a failed, unprovable transaction, StarkNet will get stuck directly: because after the transaction is forced into the account, the Prover must prove the failed transaction, but it cannot prove it.

StarkNet is expected to introduce the function of proving failed transactions in version v0.15.0, after which the Force Inclusion mechanism should be further implemented.

ForceInclusion mechanism of zkSync

zkSync's L1->L2 message transmission and Force Inclusion mechanism are both performed through the requestL2Transaction function of the MailBox contract. The user specifies the L2 address, calldata, additional ETH amount, L2GasLimit value, etc. requestL2Transaction will combine these parameters into an L2 transaction and then put it into the priority queue (PriorityQueue). When the Sequencer packages the transaction and uploads it to L1 (through the commitBatches function), it will indicate how many transactions to take out of the priority queue and include them in the L2 transaction record.

zkSync is very similar to Optimism in the form of Force Inclusion. Both use the initiator's L2 address (consistent with the L1 address) to call related functions and fill in information (callee, calldata, etc.), instead of filling in a signed L2 transaction like Arbitrum; but in design, it is the same as Arbitrum, both maintain a queue Queue in L1, and the Sequencer takes the pending transactions submitted directly by the user from the Queue and writes them into the transaction history.

If you use the official bridge of zkSync to deposit ETH, like this transaction, it calls the requestL2Transaction function of the MailBox contract, which puts the L2 transaction of the deposit ETH into the priority queue and throws a NewPriorityRequest event. Because 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 instead, you will see that the recipient of L2 in the parameters is also the initiator of the transaction (because it is a deposit to oneself), so after a while, this L2 transaction is taken out of the priority queue by Sequeuncer and included in the transaction history. It will be converted into a transaction from oneself to oneself on L2, and the amount of the transfer is the ETH amount that the initiator of the transaction brought in the Deposit ETH transaction of L1.

In the L1Deposit transaction, the transaction initiator and receiver are both 0xeDc1…6909, the amount is 0.03ETH, and calldata is empty

On L2, there will be a transaction of 0xeDc1…6909 transferring funds from oneself to oneself, and the transaction type (TxnType) is 255, which is a system transaction.

Then I called zkSync’s requestL2Transaction function just like the forced transaction function of the OP before, and sent a self-transfer: without any ETH, the calldata contained the HEX code of the “force inclusion” string.

Then it is converted into L2's last transaction to itself. The calldata is the hexadecimal string of "force inclusion": 0x666f72636520696e636c7573696f6e.

When the Sequencer takes the transaction out of the PriorityQueue and writes it into the transaction history, it will be converted into the corresponding L2 transaction on L2.

Through the requestL2Transaction function, users can use the same L1 account as the L2 address to submit data in L1, specify the L2 recipient, the attached ETH amount, and calldata. If the user wants to call other contracts with different data, just fill in the parameters one by one into the requestL2Transaction function.

There is no function to force users to include

Although the waiting period for L2 transactions to be included in the Sequencer will be calculated after the L2 transaction is placed in the priority queue, there is no Force Inclusion function that users can enforce in the current zkSync design, which is equivalent to only doing half of the work. In other words, although there is a "waiting period for inclusion", it actually depends on whether the Sequencer wants to include it: the Sequencer can wait until it expires before including it, or it can never include any transactions in the priority queue.

In the future, zkSync should add relevant functions so that users can force transactions to be included in the L2 transaction history when the income validity period has expired but has not yet been included in Sequeuncer. This is a truly effective Force Inclusion mechanism.

Summarize

L1 relies on a large number of validators to ensure the "security" and "anti-censorship ability" of the network. Rollup is even less censorship-resistant because transactions are written by a few or even a single Sequencer. Therefore, Rollup needs a Force Inclusion mechanism to allow users to bypass the Sequencer and write transactions into history to avoid being censored by the Sequencer and being unable to use or withdraw funds from the Rollup.

Force Inclusion allows users to force transactions to be written into history, but the design requires a choice of "whether transactions can be inserted into history immediately and take effect immediately". If transactions are allowed to take effect immediately, it will have a negative impact on the Sequencer, because transactions waiting to be included on L2 may be affected by transactions that are forced to be included on L1.

Therefore, the current Force Inclusion mechanism of Rollup will first put the transactions inserted on L1 into a waiting state, and allow the Sequencer a period of time to react and choose whether to include these waiting transactions.

Both zkSync and Arbitrum 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, the way zkSync sends L2 transactions is similar to Optimism, both use L2 addresses to send messages on L1. After the transaction is converted to L2, the initiator will be the L2 address. The function for sending L2 transactions in Optimism is called depositTransaction; zkSync is called requestL2Transaction. Arbitrum generates a complete L2 transaction and signs it, and then sends it through the sendL2Message function. Arbitrum will restore the signer through the signature on L2 as the initiator of the L2 transaction.

StarkNet currently does not have a Force Inclusion mechanism; zkSync is like a half set 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 only for decoration. In fact, Sequencer can Choose not to include any L2 transactions in the PriorityQueue at all