Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Force Inclusion Mechanism

Welcome to the book of Uncensored. This guide provides comprehensive information about the Force Inclusion Mechanism, including what it does, how it works and how can you use it.

What is Force Inclusion?

Force Inclusion is a mechanism that allows a L2 user to force transactions into the L2's transaction history even when the L2's sequencer is down or is actively censoring the user. Without Force Inclusion, users of the L2 won't be able to execute any transactions or even quit the L2 without sequencer's consent.

Why Force Inclusion Matters

Censorship resistance is crucial for any blockchain. If a blockchain can arbitrarily censor user transactions, it becomes no different from a Web2 server. Ethereum currently derives its censorship resistance from its numerous validators. If Alice wanted to censor Bob's transactions and prevent them from being included on-chain, she would need to either bribe every validator in the network or spam the entire network with garbage transactions that have higher fees than Bob's transactions to fill up blocks. Either approach would be extremely costly.

The Force Inclusion Solution

Rather than requiring Rollups to have as many validators as L1 to ensure censorship resistance, we can directly leverage L1's existing censorship resistance capabilities. Since Sequencers must package and submit transaction data to the Rollup contract on L1 anyway, why not add a mechanism in the Rollup contract that allows users to insert transactions into the sequencing process? This mechanism is called Force Inclusion.

As long as the Sequencer cannot censor users' "L1 transactions," it cannot prevent users from forcibly inserting Rollup transactions through L1. The operation and security of Rollups are built upon L1's censorship resistance.

Important note: Force Inclusion mechanisms must be explicitly designed into each Rollup. Users cannot automatically force inclusion of Rollup transactions through L1 unless the Rollup provides this mechanism. If a Rollup lacks Force Inclusion, users can only hope the Sequencer won't censor their transactions.

For more details, check out the series on Force Inclusion mechanisms:

  1. https://nic619.substack.com/p/force-inclusion-in-rollups-1-censorship?r=4r81
  2. https://nic619.substack.com/p/force-inclusion-in-rollups-2-implementation?r=4r81
  3. https://nic619.substack.com/p/force-inclusion-in-rollups-3-the?r=4r81
  4. https://nic619.substack.com/p/force-inclusion-in-rollups-4-solving?r=4r81

Documentation Structure

  • If you are interested in learning more about Force Inclusion mechanism and how it is implemented on different L2s, check out the Research section.
  • If you are a developer and devoted to protect your (L2) users from censorship, check out the Uncensored SDK section.
  • If you are a user and want to try out Force Inclusion, check out the Uncensored Frontend section. Though you should ask your wallet to support Force Inclusion via the Uncensored SDK which provides user experience far better than the frontend.

Force Inclusion Wallet Demo

We make some modifications to the great Rabby Wallet and prepare two videos to show you how you will be using Force Inclusion if your wallet supports it.

You can check out the Rabby Wallet modification we made to support the force inclusion feature here.

1. Uncensored Mode

If Uncensored Mode is activated, user will be able to directly force include transactions without sending them to the L2 sequencer.

The video first shows user sending a normal transaction to the L2 (Optimism Sepolia) sequencer. Next, when Uncensored Mode is activated, user will directly send force inclusion transaction to L1 (Sepolia) and eventually the transaction will be (force) included in a L2 (Optimism Sepolia) block.

  • 00:00 - 00:40: We begin with a normal deposit WETH transaction on Optimism Sepolia.
  • 00:45 - 00:50: Activate Uncensored Mode.
  • 00:50 - 01:06: Make another deposit WETH transaction but this time it is a force inclusion transaction so we will make the request on Sepolia (L1). Notice the Chain ID, address and data are all different from the previous transaction.
  • 01:06 - 01:20: Waiting for the transaction to be included in a L1 block.
  • 01:20 - 01:39: Display the transaction on L1 block explorer. Since it can take some time for the transaction to be included on L2, the video leaves out the part of displaying the transaction on L2 block explorer but you can check out the transaction here.

2. Force Include Stuck Transactions

It can happen that you send your transaction to the L2 sequencer as usual but then the sequencer goes down or is secretly censoring you. You will find your transaction stuck indefinitely. In this case, you can hit the Force Inclusion button just like you hit the "Speed Up" or "Cancel (transaction)" button, it will prompt you to sign and send a force inclusion transaction to L1 and eventually a new transaction with identical parameters will be (force) included in a L2 block.

In the video, we intentionally send a transaction with extreme low gas price so it will not be included by the L2 sequencer, to mimic the situation where the sequencer is down or is secretly censoring. Then we hit the Force Inclusion button and force include our transaction on L1.

  • 00:00 - 00:36: We begin with a normal withdraw WETH transaction on Optimism Sepolia but we set the gas price to a very low value so it will not be included by the L2 sequencer.
  • 00:36 - 00:43: View the pending transaction in the wallet.
  • 00:43 - 00:52: View the Force Inclusion button along with Speed Up button.
  • 00:52 - 01:05: Click the Force Inclusion button and view the transaction details.
  • 01:05 - 01:29: Waiting for the transaction to be included in a L1 block and display it on L1 block explorer. The video also leaves out the part of displaying the transaction on L2 block explorer but you can check out the transaction here.

Frequently Asked Questions

When should I use Force Inclusion?

Use Force Inclusion when:

  • The sequencer is offline for extended periods
  • You suspect you're being censored by the sequencer

How much does Force Inclusion cost?

Force Inclusion requests take place on L1 and bear cost, making them more expensive than regular L2 transactions. The exact cost depends on:

  • Current L1 gas prices
  • Complexity of your L2 transaction
  • The specific L2's Force Inclusion implementation

How long does Force Inclusion take?

This varies by L2:

  • Optimism: Up to 12 hours (sequencer window)
  • Arbitrum: Up to 24 hours
  • ZKsync: Currently indefinite (no enforcement mechanism)

What happens if Force Inclusion fails?

Force Inclusion can fail if:

  • Transaction parameters are incorrect
  • Insufficient gas provided for L2 execution
  • L2 transaction would fail due to state changes

Always test with small amounts first and ensure proper gas estimation.

Which L2s should I avoid if censorship resistance matters?

High Censorship Resistance:

  • Optimism, Arbitrum: Full Force Inclusion available

Medium Censorship Resistance:

  • ZKsync: Partial implementation provides some user agency

Low Censorship Resistance:

  • StarkNet: No Force Inclusion available

Force Inclusion Mechanism

Force Inclusion Mechanism allows user to force include their transaction by interacting directly with the contracts on L1 instead of going through the L2 sequencer. Hence L2 sequencer won't be able to censor users unless the sequencer also tries to do censorship on L1.

Censored by L2 sequencer

Force Include on L1

Caveats

  1. Force Inclusion guarnatees "Inclusion", not "Execution"
    • meaning user can force include his transaction but the result of execution might not be what he is expecting, e.g., the transaction might fail
  2. The handoff problem: check out this great writeup about the limitations of Force Inclusion mechanism here

Different L2, different implementation

Implementation of Force Inclusion Mechanism varies from one L2 to another. Here are some examples:

How to make the force inclusion transaction request?

  • Optimism: user takes parameters of a normal Optimism transaction and sends them to the L1 contract to make the request
  • Arbitrum: user takes a normal signed Arbitrum transaction and sends it to the L1 contract to make the request

Does "who interacts with the L1 (Force Inclusion) contract" matter?

  • Optimism: user needs to interact with the L1 contract himself
  • Arbitrum: a third party can interact with the L1 contract on user's behalf

How is the gas cost of the force inclusion transaction paid?

  • Optimism: gas cost of the force inclusion transaction is paid on L1
  • Arbitrum: gas cost of the force inclusion transaction is paid on Arbitrum, just like a normal Arbitrum transaction

Does user need extra steps to complete the force inclusion?

  • Optimism: user does not need extra steps to complete the force inclusion
  • Arbitrum: user needs to complete the force inclusion manually after a 24 hour delay, if sequencer chooses to omit the force inclusion request

Force Inclusion on Optimism

Check out Force Inclusion on Optimism


Force Inclusion on Arbitrum

Check out Force Inclusion on Arbitrum


After you finish reading the above, you can check out the Uncensored SDK to see how you can implement Force Inclusion Mechanism in your wallet or the Uncensored Front End to see how to use the frontend to force include your transaction.

Force Inclusion on Arbitrum


Caveats

  • The user is assumed to be an EOA. If the user is using a contract account, the process will be a little different and is not covered in this document.

Flow

Steps to successfully complete a force inclusion on Arbitrum:

  1. Sign Normal L2 Transaction: Sign the normal L2 transaction as usual. This will be the force included transaction.
  2. Request: Make the force inclusion request from L1. In Arbitrum's case, it's the Inbox contract on L1.
  3. Transaction ID: Get the identifier to the force included L2 transaction in the request. In Arbitrum's case, it's the transaction hash of the normal L2 transaction signed in step 0.
  4. Track Progress: Tracking the progess of the force included L2 transaction in the request. In Arbitrum's case, query for the transaction's receipt using getTransactionReceipt RPC call.
  5. Complete Inclusion: In Arbitrum's case, sequencer has around 24 hours to include the force included L2 transaction. If sequencer fails to do so, anyone can complete the force inclusion via the SequencerInbox contract on L1.

Contracts Used

0: Sign Normal L2 Transaction

The users signs a normal L2 transaction as he used to. Or if he already signed and sent his L2 transaction but the transaction never gets included, then he can just use the transaction to make the Force Inclusion request.

1: Request

The user calls the sendL2Message function on Inbox contract on L1:

function sendL2Message(bytes calldata messageData)

Important notes:

  • The user does not have to be the one sending the L1 transaction to call sendL2Message function. He can have a relayer carry his signed normal L2 transaction onchain.
  • The function has two modifiers: whenNotPaused and onlyAllowed
    • Rollup owner can pause the contract, effectively disabling force inclusion.
    • Allow list is currently not enabled. If enabled, it will check if the tx.origin is on the allow list, effectively disabling force inclusion by anyone except the ones in the allow list. Only Rollup owner can set allow list.

Parameters

To better explain the parameters, we will use an example: suppose Bob wants to force include his transaction which executes an ETH->USDC swap on Uniswap on Arbitrum.

Bob will first sign the L2 transaction which will execute an ETH->USDC swap on Uniswap on Arbitrum. Next he will append 0x04 to the signed L2 transaction payload: 0x04 | Bob's signed transaction. This will be the messageData param to the sendL2Message function. See the sendChildSignedTx function in Arbitrum SDK as an example.

Important notes:

2. Transaction ID

Once the request is made successfully, a MessageDelivered event will be emitted. The event will contain all data necessary to complete the force inclusion in step 4. You can use the getForceIncludableEvent function in Arbiturm SDK to extract MessageDelivered logs . However, that function will exclude any force inclusion transactions that is not eligible yet, i.e., it has not passed the 24 hours Force Inclusion Grace Period. Hence if you wish to extract MessageDelivered logs before the grace period ends, you will have to copy and modify the function.

Since the user is force including his signed normal L2 transaction, naturally the identifier for the force included L2 transaction is the transaction hash of his signed normal L2 transaction. You can use ``ethers.utils.parseTransaction` to get the L2 transaction hash


3. Track Progess

With the L2 transaction hash, you can query APIs/RPCs to chekc its status or query the explorer. Or if you are using libraries like Viem, you can use `waitForTransactionReceipt. But note that it could take up to 24 hours for the transaction to be included.


4. Complete Inclusion

In Arbitrum's case, currently sequencer has around 24 hours grace period to include user's transaction. If the sequencer did not include it in 24 hours. Anyone can call the forceInclusion function on SequencerInbox contract on L1. The grace period on Ethereum is currently set to 5760 L1 blocks (24 hours).

Parameters

The emitted MessageDelivered event basically contains all data needed to call the forceInclusion function. The parameters will be used to compute the message hash and check if it matches the one stored in the transaction queue. Message hash is stored in the transaction queue when user called sendL2Message to make the force inclusion request, i.e., step 1.

Code Walkthrough

Message hash computation in sendL2Message function:

bytes32 messageHash = Messages.messageHash(
    kind,
    sender,
    blockNumber,
    blockTimestamp,
    count,
    baseFeeL1,
    messageDataHash
);

The data used to compute message hash will be emitted in the MessageDelivered event in sendL2Message function:

emit MessageDelivered(
    count,
    prevAcc, // not used by `Messages.messageHash()`
    msg.sender, // not used by `Messages.messageHash()`
    kind,
    sender,
    messageDataHash,
    baseFeeL1,
    blockTimestamp
);

Message hash computation in forceInclusion function:

function forceInclusion(
        uint256 _totalDelayedMessagesRead,
        uint8 kind,
        uint64[2] calldata l1BlockAndTime,
        uint256 baseFeeL1,
        address sender,
        bytes32 messageDataHash
    ) external {
    ...
    bytes32 messageHash = Messages.messageHash(
        kind, // the `kind` in `MessageDelivered` event
        sender, // the `sender` in `MessageDelivered` event
        l1BlockAndTime[0], // not emitted by the `MessageDelivered` event but you will get it when filtering for event logs
        l1BlockAndTime[1], // the `blockTimestamp` in `MessageDelivered` event
        _totalDelayedMessagesRead - 1, // the `count` parameter in `MessageDelivered` event
        baseFeeL1, // the `baseFeeL1` in `MessageDelivered` event
        messageDataHash // the `messageDataHash` in `MessageDelivered` event
    );
    ...
}

Gas

The gas cost of a force inclusion transaction is not paid on L1, like Optimism. Instead, it is paid on L2, when the force included transaction is finally included and executed.

L2 Gas Limit and L2 Gas Price

User should simulate his L2 transaction, get the estimated gas limit and gas price for his transaction. Perhaps adding a buffer because the force inclusion transaction can take up to 24 hours before inclusion and execution.


Transaction Replacement

Force inclusion transaction is not replaceable.


Reasons Force Inclusion transaction could fail

1. Not enough ETH to transfer

User should make sure he has enough ETH on Arbitrum if he wants to transfer ETH in his force inclusion transaction.

2. Not enough L2 gas limit

User should make sure he sets high enough gas limit so that his force inclusion transaction does not run into "Out of Gas" error.

3. Gas price too low

User should make sure he sets high enough gas price so that his force inclusion transaction does not get rejected due to low gas price.

4. L2 execution failed

If force inclusion transaction is not included and executed in time, the transaction might fail. For example, Bob's force inclusion transaction which swaps ETH to USDC on Uniswap Pool might fail because the price has moved quite a bit since he makes the request on L1.


Side effects of Force Inclusion transaction

EOA's nonce on L2 will be incremented

Nonce of user's address on L2 will be incremented whenever his force inclusion transaction is included and executed, no matter the transaction fail or not. This could result in wallet crafting a transaction with wrong nonce if it was not aware of force inclusion transactions.

Force Inclusion on Optimism


Caveats

  • The user is assumed to be an EOA. If the user is using a contract account, the process will be a little different and is not covered in this document.

Flow

Steps to successfully complete a force inclusion:

  1. Craft a normal (unsigned) L2 transaction: This is the transaction that you want to force include but do not sign it.
  2. Request: Make the force inclusion request from L1. In Optimism's case, it's the OptimismPortal contract on L1.
  3. Transaction ID: Get the identifier to the force included L2 transaction in the request. In Optimism's case, it's the transaction hash of the force included L2 transaction. Unlike a normal L2 transaction, it will incorporate information like L1 blockhash and log index. See below for detail.
  4. Track Progress: Tracking the progess of the force included L2 transaction in the request. In Optimism's case, query for the transaction's receipt using getTransactionReceipt RPC call.
  5. Complete Inclusion: In Optimism's case, no manual intervention is needed to complete the inclusion. It's up to sequencer to decide when to include the transaction. Sequencer has SEQEUNCER_WINDOW amount of time before the transaction is force-included.

Contracts Used

0: Craft a normal (unsigned) L2 transaction

The users crafts a normal L2 transaction which he wish to execute. Or if he already signed and sent the L2 transaction but the transaction never gets included, then he can just use the transaction data to make the Force Inclusion request.

1: Request

The user calls the depositTransaction function on OptimismPortal contract on L1:

function depositTransaction(
    address _to,
    uint256 _value,
    uint64 _gasLimit,
    bool _isCreation,
    bytes memory _data
)

Important notes:

  • The user MUST be an EOA.
  • The user MUST send a L1 transaction to execute this function using his OEA. He can not use a meta-transaction and have a relayer send the L1 transaction on his behalf.

Parameters

To better explain the parameters, we will use an example: suppose Bob wants to force include his transaction which executes an ETH->USDC swap on Uniswap on Optimism.

  • _to: the destination address on L2, i.e., "who you want to call on L2?"
    • in the example, this is the address of the Uniswap ETH-USDC Pool on Optimism
  • _value: the amount of ETH to deposit to L2, i.e., "how much ETH you want to deposit to your address along with the request?"
    • in the example, _value will be 0 as Bob does not want to deposit ETH to his address from L1 to L2. He just wants to swap ETH to USDC on L2.
  • _gasLimit: the gas limit of the force-included L2 transaction, i.e., "how much gas you need to execute your transaction on L2"
    • in the example, this is the gas limit to executes the swap. Bob should simulate the swap and record the gas limit after the simulation.
  • _isCreation: indicates if this is a transaction which deploys a new contract on L2
    • in the example, _isCreation will be 0 as Bob does not want to deploy a new contract on L2.
  • _data: the data to call the destination address on L2, i.e., "what do you want to do on L2?"
    • in the example, this is the calldata encoding the function and the parameters for the swap

Important notes:

  • There's a minimum _gasLimit value depending on the size of _data.
    • if (_gasLimit < minimumGasLimit(uint64(_data.length))) revert SmallGasLimit();
    • minimumGasLimit
  • Size of _data can not exceed 120k bytes
    • if (_data.length > 120_000) revert LargeCalldata();
  • The user will be charged on L1, not L2, for the amount of gas to spend on L2, i.e., the _gasLimit value

2. Transaction ID

Once the request is made successfully, a TransactionDeposited event will be emitted. A L2 transaction (Deposited Transaction) will be derived from this event and waits to be included by sequencer. You can use Viem to extract TransactionDeposited logs

A force inclusion transaction is uniquely identified by

  • Source Hash: Source hash is used to differentiate two deposits with same parameters. A source hash is comprised of
    • The L1 Blockhash during which the TransactionDeposited event was emitted
    • The index of the emitted TransactionDeposited log
  • and other parameters specified in the depositTransaction function

Computing Source Hash

Computing L2 Transaction Hash

This is the Transaction ID used to track the progess of the force inclusion transaction.

  • Spec
    • It will be a new transaction type defined in Optimism, with slightly different transaction fields.
    • Just like a normal Optimism transaction, you fill out the fields in the transaction, rlp encode it and then keccak hash it to get the transaction hash.
  • Implementations
    1. Viem
    2. Optimism Official

3. Track Progess

With the computed L2 transaction hash, you can query APIs/RPCs to chekc its status or go to explorer and wait for the transaction to show up. Or if you are using libraries like Viem, you can use waitForTransactionReceipt. But note that it could take up to 12 hours for the transaction to be included.


4. Complete Inclusion

In Optimism's case, no manual intervention is needed to complete the inclusion. It's up to sequencer to decide when to include the transaction, however, sequencer has SEQEUNCER_WINDOW amount of time before the transaction is force-included.

Currently SEQEUNCER_WINDOW on Ethereum is set to 12 hours (3600 L1 blocks).


Gas

L2 Gas Limit

User should simulate his L2 transaction, record the gas limit after the simulation and use that value as the gas limit for his force inclusion transaction. Perhaps adding a buffer because the force inclusion transaction can take up to 12 hours before inclusion and execution.

L2 Gas Price

Since gas fee is paid on L1, users don't have to set L2 gas price for their force inclusion transaction.


Transaction Replacement

Force inclusion transaction is not replaceable.


Reasons Force Inclusion transaction could fail

1. Not enough ETH to transfer

User should make sure he has enough ETH on Optimism if he wants to transfer ETH in his force inclusion transaction. Alternatively he can deposit the ETH from L1 in his force inclusion transaction.

2. Not enough L2 gas limit

User should make sure he sets high enough gas limit so that his force inclusion transaction does not run into "Out of Gas" error.

3. L2 execution failed

If force inclusion transaction is not included and executed in time, the transaction might fail. For example, Bob's force inclusion transaction which swaps ETH to USDC on Uniswap Pool might fail because the price has moved quite a bit since he makes the request on L1.


Side effects of Force Inclusion transaction

EOA's nonce on L2 will be incremented

Nonce of user's address on L2 will be incremented whenever his force inclusion transaction is included and executed, no matter the transaction fail or not. This could result in wallet crafting a transaction with wrong nonce if it was not aware of force inclusion transactions.

Uncensored SDK

We have test scripts that use the SDK to make force inclusion requests on testnets. For now, please refer to the scripts on how to use the SDK. It's actually very simple!

You can also check out the Rabby Wallet modifiication we made to support the force inclusion feature. Here is the link to the code change.

NOTE: Only Optimism Stack Chains are supported right now

Uncensored Frontend

If your wallet does not support Uncensored SDK, you can use the Uncensored Frontend to force include your transactions.

NOTE: Only Optimism Chains are supported right now

You can try out the Uncensored Frontend here. (Testnet version)

How to use

  • Get the transaction data - check out the Send Transaction section.
  • If the network you are on does not show up in the dropdown - check out the Custom Network section.
  • If you want to see the transaction history - check out the Transaction History section.

Uncensored Frontend - Send Transaction

Here we will go over different ways to use the Uncensored Frontend.

1. Build your transaction from scratch

Just like Safe Wallet's Transaction Builder, you can build your transaction on the Uncensored Frontend, supplying it with the contract address you want to interact with and its ABI. The frontend will try to fetch the ABI from explorer, if it fails, you can manually supply it.

Send Transaction - Address and Compose Data

Send Transaction - ABI auto fetched and Proxy

Next you can click the Select function button to select the function you want to call from the dropdown, fill in the parameters, and click Generate Data.

Send Transaction - Select Function and Fill in Parameters

If your transaction requires sending ETH along the way, remember to fill in the value (ETH) field.

Lastly, the tricky part is to estimate the gas limit of this transaction. Right now you have to fill in the gas limit manually. In the future, we plan to support automatic gas estimation.

Send Transaction - Value and Gas Limit

Once all the fields are filled in, click Submit to send out the transaction.


2. Copy from existing transaction

It could happen that you already sent a transaction but it never gets included or if you find it difficult to build the transaction from scratch. In either case, you can copy the fields from an existing transaction.

Send Transaction - Copy from Existing Transaction

2.1 Copy from a stuck transaction

You should be able to view the stuck transaction's detail either directly on the wallet's transaction history page or on the explorer. Then you can copy the to, value, data, and gasLimit fields from the transaction details.

If your wallet does not show specific fields like data and the transaction also does not show up on the explorer, you might have to try the next approach.

2.2 Interact with DApp and copy from prompted transaction request

If you are interacting with a DApp, for example, interacting with Uniswap frontend to execute a swap. After you have chosen the pair and the amount and clicked Swap, your wallet will be prompted with transaction signing request. Then you can copy the to, value, data, and gasLimit fields from the transaction viewing page.

Send Transaction - Copy from DApp Transaction

Uncensored Frontend - Custom Network

If you don't find the chain you want on the Uncensored Frontend, you can add it as a custom network.

NOTE: Only Optimism Stack Chains are supported right now

After you click the chain button, you will see a dropdown menu of supported chains. Click Add Network to add a custom network.

Custom Network - Add Network Button

In the Add Network modal, you need to fill in the following fields:

  • Network Name: The name of the chain you want to add.
  • Chain ID: The chain ID of the chain you want to add.
  • Portal Proxy Address: The address of the L1 (Optimism Portal) contract to make the force inclusion request.
    • For example, you can find the Portal Proxy Address of Blast from their docs. Look for the address of OptimismPortal.
    • Or check out L2Beat's detailed documentation of each L2 chain and look for OptimismPortal in the Smart Contracts section.
      • Custom Network - L2Beat Smart Contracts
      • Custom Network - L2Beat Smart Contracts Optimism Portal
  • RPC URL: The RPC URL of the chain you want to add.

After you fill in the fields, click Add to add the custom network.

Custom Network - Add Network Example

Uncensored Frontend - Transaction History

You can view your force inclusion transaction history on the top right corner (History tab) of the Uncensored Frontend.

For one force inclusion, you will see a pair of one L1 transaction and one L2 transaction. L1 transaction is the transaction that you sent on the L1 chain to make the force inclusion request, and L2 transaction is the transaction that will be (force) included and executed on the L2 chain.

Transaction History - Transactions Pair