Original author: NIC Lin

Original source: medium

May the Force of Inclusion be with you. This article will introduce Rollup’s transaction anti-censorship mechanism — Force Inclusion, and use the design and practice of several famous Rollups as examples.

Photo by Artur Tumasjan on Unsplash

Prerequisites:

  • Understand how Rollup works and why Rollup needs to upload transactions to L1

Censorship-resistance of transactions

The ability to resist censorship is very important for a blockchain. If a blockchain can censor transactions for any user, then it is no different from a Web2 server. Ethereum's current ability to resist censorship comes from a large number of validators. If Alice wants to censor Bob's transactions and prevent his transactions from being put on the chain, she must either try to bribe every validator in the network or spam the entire network and continuously send out junk transactions that are higher than Bob's transactions to fill up the blocks. Either way, her cost will be very high.

Note: In Ethereum’s current PBS architecture, the cost of review is indeed significantly reduced. You can refer to the percentage of blocks that OFAC reviews Tornado Cash transactions. 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 its security. In addition, Rollup only has one centralized role (Sequencer, called sorter or sorter) to batch blocks, and it is as secure as L1. But security and anti-censorship capabilities are two different things. If a Rollup is built to be as secure as Ethereum, but there is only one centralized Sequencer, then the Sequencer can censor any user's transactions if it wants.

Sequencer can reject incoming user transactions, causing users to be unable to use or leave Rollup

Forced inclusion mechanism

Rather than requiring the same number of validators for Rollup and L1 to ensure censorship resistance, it is better to directly utilize L1's censorship resistance: the sequencer also compresses transaction data into the L1 Rollup contract. It is better to add a method in the Rollup contract that allows monitoring users to insert transactions into the sequence. This method is called forced inclusion. As long as the Sequencer cannot censor the "L1 transaction" of the monitoring user, it cannot prevent the user from forcibly inserting Rollup transactions through L1, and the operation and security foundation of Rollup is based on L1's censorship resistance.

The sorter cannot censor a user’s L1 transaction without significant overhead

Note: Forced inclusion is only possible if the Rollup is designed. Users can definitely break through L1 to forcefully insert Rollup transactions. If the Rollup does not provide a forced inclusion mechanism, users can only pray that the Sequencer censors the transactions themselves.

Immediate vs. Delayed Transactions

If we allow transactions inserted through Force Inclusion to be directly written into the Rollup's transaction history (suddenly taking effect immediately), the state of the Rollup will be changed immediately. For example, Bob inserts a transaction "he transfers 1000 DAI to Carol" through the Force Inclusion mechanism. If the transaction takes effect immediately, then in the latest state, Bob's balance will be 1000 DAI less and Carol will have 1000 DAI more (of course, the premise is that Bob's balance exceeds 1000 DAI).

If Bob's transaction can be directly written into the transaction history and take effect immediately, the state of the Rollup will change immediately.

If the Sequencer is collecting transactions off-chain at this time, and is waiting to send the next transaction to the Rollup contract, it may be affected by the transaction that Bob forcibly inserts and takes effect immediately. For example, Bob only needs to send a payment transaction "he transfers 1000 DAI to Alice" to the Sequencer in advance. The Sequencer is equivalent to verifying that there is no problem and promising to receive it. Both Alice and Bob can query the Sequencer whether the transaction will be received and calculate the latest status (Alice has 1000 DAI more and Bob has 1000 DAI less). But when the Sequencer finds that Bob has forced to insert the transaction first, the execution status of the transactions in its hands has changed. It knows that the transaction "Bob transfers 1000 DAI to Alice" has become a failed execution, and Alice cannot get 1000 DAI.

Sequencer receives Bob’s transaction, so Alice believes she will receive 1000 DAI

As a result, Bob directly forced another conflicting transaction to be collected on L1, causing the transaction in the hands of the Sequencer to be unable to be collected, and Alice could not get DAI

This may not be a good user experience, so Rollup generally makes the mandatory inclusion of transactions take effect immediately, but advances the transaction to a "preparing" state. The Sequencer can choose whether to insert these "preparing" state transactions to the end of the batch when the budget transaction is sent up. If the Sequencer does not want to deal with these "preparing" states, then these transactions can be forced to be inserted into the transaction history after a period of time.

Bob wants to cheat Alice by forcing an income transaction, but in reality the transaction will go first into the waiting queue

The Sequencer can decide when to "include" transactions waiting in the queue, so Bob's transaction to Alice DAI will be executed first.

If the Sequencer has not been waiting for transactions in the queue, after a period of time the user (or anyone else) can force the revenue themselves.

Sequencer rejects Bob’s transaction, so Bob puts the transaction in the waiting queue from L1.

The sequencer can still continue to reject transactions in the queue for a period of time

However, the Sequencer cannot always reject transactions in the waiting queue. After a period of time, anyone can trigger forced income. Next, the implementation of the force inclusion mechanism of more famous Rollups such as Optimism, Arbitrum, StarkNet and zkSync will be introduced in turn.

Optimistic power inclusion mechanism

First, let's introduce the Deposit process of Optimism. This Deposit does not only mean depositing money into Optimism, but is more generally equivalent to "depositing a message to L2" into L2. After receiving the newly deposited message, the L2 node transmits the message to the port L2 transaction for execution and transmits the message to the specified recipient.

Message of user depositing from L1 to L2

L1CrossDomainMessenger Contract

When a user decides to deposit ETH or ERC-20 tokens into Optimism, he will interact with the L1StandardBridge contract on L1 (cross-webpage), specifying how much to deposit and which address to receive, etc. Then L1StandardBridge connects the message to the L1CrossDomainMessenger contract on the next layer. This contract is mainly used as a general contract for mutual communication between L1 and L2. L1StandardBridge uses this general communication contract to communicate with L2StandardBridge on L2 to decide who can mint tokens from L2 or withdraw tokens from L1. If a developer needs to develop a contract for mutual communication and synchronization between L1 and L2, he can build it on L1CrossDomainMessenger.

User messaging CrossChainMessenger contract is passed from L1 to L2

OptimismPortal Contract

The L1CrossDomainMessenger contract will then send the message to the bottom-level OptimismPortal contract. After the OptimismPortal contract completes the processing, it will issue a TransactionDeposited event. The event parameters include the "sender of the message", "receiver of the message", and related execution parameters.

Then the L2 Optimism node will listen to the TransactionDeposited event issued by the OptimismPortal agreement, and convert the parameters in the event into L2 transactions. The initiator of this L2 transaction may be the "message sender" in the TransactionDeposited event, and the recipient of the transaction will be the "message receiver" in the event. Other execution parameters are also passed in from the event parameters.

L2 nodes insert TransactionDeposited events emitted by OptimismPortal into annotated L2 transactions

For example, this transaction is a user depositing 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. The message initiator is the L1CrossDomainMessenger contract; the receiver is the L2CrossDomainMessenger contract on L2; and 0.01 ETH is attached.

Forced inclusion

When a user wants to force his transaction into optimistic execution, he does not need to send a message from L1 to L2, so he will not use the L1CrossDomainMessenger contract. The effect he wants to achieve is to make the bill specific "the transaction he sent from his" L2 address on L2 to be executed smoothly, so he will interact directly with the OptimismPortal contract and let his L2 address call the OptimismPortal contract, so that when the TransactionDeposited event is converted into an L2 transaction, the "transaction initiator" will be his L2 address, and then he can see the transaction again.

In the L2 transaction converted from the TransactionDeposited 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

Use the L2 address to call the depositTransaction function signed by OptimismPortal, and remember to fill in the parameters of the L2 transaction one by one

I made a simple forced inclusion transaction: I sent money to myself from my L2 address (0xeDc1…6909) with a “force inclusion” message. This is my L1 transaction executing the depositTransaction function through the OptimismPortal contract. You can see that in the emitted TransactionDeposited event, both from and to are me. The remaining value in the opaqueData field (0 ... ETH to L2 recipient", "L2 transaction Gas Limit" and "Data to L2 recipient" and other information.

TransactionDeposited event emitted by L1 transaction

After interpreting the values ​​of the opaqueData information, we can get:

  • How much ETH does the person calling DepositTransaction have? 0 because I don’t want to deposit ETH from L1 to L2

  • How much ETH should the L2 transaction initiator attach to the L2 recipient? 5566 (wei), because I want to transfer money to myself

  • L2 transaction gas limit: 50000

  • "Data for L2 receiver": 0x666f72636520696e636c7573696f6e, clearly "mandatory inclusion" of the hexadecimal encoding of this string

Soon after, the converted L2 transaction appeared: I wrote down the L2 transaction in which I transferred money to myself, the amount was 5566, and the data was the "mandatory inclusion" string. And you can notice that in the Other Attributes of the second-to-last line in the figure, the Txn Type (transaction type) is system transaction 126 (System), which means that it is not a transaction initiated by me in L2, but converted from the L1TransactionDeposited event.

Conversion completed L2 transaction

If the user wants to call other contracts with different data, then fill in the parameters one by one into the depositTransaction letter. Just remember to use the L2 address to execute on L1, so that the L2 transaction initiator will be the L2 address.

Sequencer Window

The Optimism L2 node mentioned above converts the TransactionDeposited event into an L2 transaction. In fact, this Optimism node refers to the Sequencer node. After all, this is related to transaction ordering, so only the Sequencer can decide when to convert to an L2 transaction. When monitoring the TransactionDeposited event, the Sequencer may not immediately convert the event into an L2 transaction, but it can decide to convert it after a period of time. This period of time is called the sequencer window. The current sequencer window on the Optimism main network is 24 hours. So when a user deposits money or a reminder from L1, or a mandatory transaction to include a payment, the worst case scenario will be that it will not be officially included in the L2 transaction history until 24 hours later, but at least this is better than the result that the transaction will never be included.

Arbitrum’s forced inclusion mechanism

In the optimistic scenario, the deposit operation of L1 will emit a TransactionDeposited event, and the rest is to wait for the Sequencer to receive the deposit operation; but in the arbitrary scenario, the operation of L1 (depositing money or sending messages to L2, etc.) will be stored in a queue agreed by L1, instead of just emitting events. The sequencer will be given a period of time to add the L1 operation transaction history in this queue. If the sequencer is done by the time, then no one needs to replace the sequencer and it can be completed.

The arbitration will maintain a queue in the L1 contract. If the Sequencer does not actively collect transactions in the queue, anyone can force the transactions in the queue to be included in the transaction history when the time is up.

All L1 operations are contracts of Delayed Inbox. As the name implies, the operations here will be delayed and invalid. Another contract is Sequencer Inbox, which is the interface for Sequencer to upload L2 transactions. The transactions uploaded by Sequencer will be directly written into the transaction history. When uploading, Sequencer can choose how many L1 operations to take from Delayed Inbox and write them into the transaction history together to make these L1 operations effective.

SequencerInbox contains transaction history. Generally, only Sequencer can directly write new transactions. DelayedInbox contains transactions waiting to be collected.

When Sequencer writes a new transaction, it can also initiate a transaction from DelayedInbox to write it together

Complex design and impressive documentation

If readers directly refer to Arbitrum's official chapter on Sequencer and Force Inclusion, they will see how Force Inclusion is compatible, as well as some parameter names and function names: the user first goes to Inbox to agree to call the sendUnsignedTransaction function. If the Sequencer does not receive the transaction within about 24 hours, the user can call the forceInclusion function signed by the Sequencer Inbox. That's it, there is no link attached, and you can only agree on the corresponding function in the solution code yourself.

When you find the sendUnsignedTransaction function, you realize that you actually need to fill in the nonce value and maxFeePerGas value yourself. Which address's nonce is it? Which network's maxFeePerGas value is it? How should I fill it in? There is no documentation, not even Natpsec. Then you also find a function with a lot of skins: sendL1FundedUnsignedTransaction, sendUnsignedTransactionToFork, sendContractTransaction, sendL1FundedContractTransaction. There is also no documentation telling you the difference between these functions, how to use them, and how to fill in the parameters, not even Natpsec.

You try to fill in the parameters and send the transaction with a comprehensive consideration and a try-it-out mentality, hoping to find the right method through trial and error, but you find that all these functions will use your L1 address as an address alias, resulting in the transaction initiator on L2 being a fundamentally different address, so your L2 address does not change at all.

Sending L2 Messages

Later, I accidentally clicked on a link on the Google search page and found that Arbitrum itself had a tutorial library, which contained scripts demonstrating how to send L2 transactions from L1 (or what Force Inclusion means). However, its function is completely different from any of the above mentioned functions. It is just a function called sendL2Message, and the message parameter is actually the signed Arbitrum L2 transaction? ? ? Who would have known that to "send an L2 message" would actually be a note "a signed L2 transaction"? ? And once again, there is no document or Natspec explaining when and how to use this function.

Conclusion: It is troublesome to manually generate an Arbitrum mandatory income transaction. It is recommended to run the Arbitrum SDK according to the official tutorial. Arbitrum is not like other Rollups that have clear developer documents and plan code annotations. The purpose and parameters of many functions are not explained, causing developers to spend more time than expected to access and use them. I also asked Arbitrum people on the Arbitrum Discord, but did not get a satisfactory answer.

Note: When I asked on Discord, the other party just told me to listen to sendL2Message, without explaining the purpose of other functions (even sendUnsignedTransaction mentioned in the mandatory include file), how to use it, and when to use it.

Unfortunately, StarkNet does not yet have mandatory inclusion. There are only two posts on the official forum that discuss censorship and mandatory inclusion mechanisms.

Unable to prove transaction failure

The reason is that StarkNet's zero-knowledge proof system cannot prove that a transaction has failed to be recorded, so it cannot be forced to be recorded. Because if someone maliciously (or unintentionally) forces the recording of a transaction that has failed and cannot be proven, StarkNet will get stuck directly: because after the transaction is forced to be recorded, 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 it should be able to further implement the forced inclusion mechanism.

zkSync’s mandatory inclusion mechanism

zkSync's L1->L2 message passing and forced inclusion mechanism are all performed through the requestL2Transaction function of the MailBox contract. The user specifies the L2 address to be called, call data, the amount of ETH carried, the L2 Gas Limit value, etc., splits these parameters of requestL2Transaction into an L2 transaction and then simulates the priority queue. The Sequencer will specify how many transactions to take out of the priority queue when uploading the transaction backup to L1 (commitBatches function).

zkSync is very similar to Optimistic in its forced inclusion interface. It uses the user’s L2 address to make calls and fills in relevant data (the callee, call data, etc.), rather than filling in signed L2 transactions like Arbitrum. However, in terms of design, transactions are maintained in an entity queue on L1 like Arbitrum, and the Sequencer takes transactions from the queue and writes them into the transaction history.

Users insert L2 transactions into the priority queue via requestL2Transaction, and Sequencer can pick up transactions from the priority queue when commitBatches

If you deposit ETH through the official bridge of zkSync, it is equivalent to a transaction at this time. It calls the requestL2Transaction function of the MailBox contract, which will keep the L2 transaction priority queue of the deposited ETH and issue 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 the L1 transaction instead, you will see that the recipient of L2 in the parameters is also the initiator of the transaction (it is Deposit to oneself), so after a while, when the exchange L2 transaction is deducted from the priority queue by the sequencer and included in the transaction history, it will be converted on L2 to record the transaction transferred from itself to itself, and the amount of the transfer is the amount of ETH that the initiator of the transaction brought to the deposit ETH exchange of L1.

In the L1 recharge transaction, the transaction initiator and receiver are both 0xeDc1…6909, the amount is 0.03 ETH, and the call data is empty

L2 will record 0xeDc1…6909, which is a transaction from oneself to oneself. The transaction type (Txn Type) is 255, which is a mysterious system transaction.

Next, I called the requestL2Transaction function directly, sending a transaction that calls itself without any ETH, and the hexadecimal code of the "mandatory inclusion" string in the call data. The subsequent formula is converted into a transaction that calls itself on L2, with the "mandatory inclusion" string in the call data: 0x666f72636520696e636c7573696f6e.

When the sorter takes the transaction out of the priority queue and writes it into the transaction history, it can be converted into a corresponding L2 transaction on L2.

Through the requestL2Transaction function, users can request L2 transactions on L1 with L2 addresses, specify the L2 recipient, the ETH amount and call data. If the user wants to call other contracts with different data, just fill in the parameters one by one into the requestL2Transaction function. Just remember to use the L2 address to execute on L1, so that the L2 transaction initiator will be the L2 address.

There is no feature that allows users to force income

Although placing L2 transactions in the priority queue will calculate the deadline for L2 transactions to be collected by the Sequencer, the current zkSync design does not allow users to execute the mandatory inclusion related functions themselves, which is equivalent to doing a half-hearted job. So although there is an "effective period for collection", it actually depends on "whether the Sequencer wants to collect it": the Sequencer can wait until after the transition to collect it, or it can never collect any transactions in the priority queue. In the future, zkSync should add relevant functions to allow users to force collection when the collection has passed but has not been collected by the Sequencer. This is a truly effective mandatory inclusion mechanism.

Summarize

  • L1 relies on a group of people to ensure the "security" and "censorship resistance" of the network, while Rollup relies on "L1's censorship resistance" to obtain "the same security as L1", but does not include Rollup's censorship resistance.

  • On the contrary, since Rollup is written by a few or even a single Sequencer, its anti-censorship ability is weaker. 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 leave Rollup.

  • Forced 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 cause trouble for the sequencer and difficulty in user experience, because transactions waiting to be included on L2 may be affected by transactions that are forced to be included on L1.

  • Therefore, the current forced inclusion mechanism of Rollup will first put the transactions inserted on L1 into a waiting state, and give the Sequencer a window of time to react and choose whether to include these waiting transactions.

  • Both zkSync and Arbitrum maintain a physical queue on L1 to manage L2 transactions or messages sent from L1 to L2. Arbitrum calls it a delayed inbox; zkSync calls it a priority queue.

  • However, zkSync’s way of sending L2 transactions is similar to Optimism, both of which use the L2 address to send transactions on L1, so the initiator of the L2 transaction will be the L2 address. The function used by Optimism to send L2 transactions is called depositTransaction; zkSync is called requestL2Transaction. Arbitrum creates 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 has no forced inclusion mechanism; zkSync seems to have done a half-hearted forced inclusion - there is a priority queue and each L2 transaction in the queue has an effective period, but this effective period is currently just for decoration. In fact, the sequencer can choose not to include any L2 transactions in any priority queue at all.