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:
- https://nic619.substack.com/p/force-inclusion-in-rollups-1-censorship?r=4r81
- https://nic619.substack.com/p/force-inclusion-in-rollups-2-implementation?r=4r81
- https://nic619.substack.com/p/force-inclusion-in-rollups-3-the?r=4r81
- 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.
Caveats
- 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
- 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
- Flow
- Gas
- Transaction Replacement
- Reasons Force Inclusion transaction could fail
- Side effects of Force Inclusion transaction
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:
- Sign Normal L2 Transaction: Sign the normal L2 transaction as usual. This will be the force included transaction.
- Request: Make the force inclusion request from L1. In Arbitrum's case, it's the
Inbox
contract on L1. - 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.
- 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. - 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
andonlyAllowed
- 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.
- Rollup owner can
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:
- The size of
messageData
can not exceed117964
bytes (~116 KB)
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
- Flow
- Gas
- Transaction Replacement
- Reasons Force Inclusion transaction could fail
- Side effects of Force Inclusion transaction
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:
- Craft a normal (unsigned) L2 transaction: This is the transaction that you want to force include but do not sign it.
- Request: Make the force inclusion request from L1. In Optimism's case, it's the
OptimismPortal
contract on L1. - 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.
- 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. - 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 be0
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.
- in the example,
_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 be0
as Bob does not want to deploy a new contract on L2.
- in the example,
_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 bytesif (_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- charge for
_gasLimit
indepostiTransaction
- metered: the gas metering modifier
- charge for
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
- The L1 Blockhash during which the
- and other parameters specified in the
depositTransaction
function
Computing Source Hash
- Spec
- Implementations
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
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.
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
.
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.
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.
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.
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.
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 ofOptimismPortal
. - Or check out L2Beat's detailed documentation of each L2 chain and look for
OptimismPortal
in theSmart Contracts
section.
- For example, you can find the
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.
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.