Rollup Integration
Overview
The Safe Sequencer will be integrated into many rollup frameworks. Our first integration is with the OP Stack. If you want to use a framework other than the OP Stack please let us know.
Sequencer operators are easily changed on most rollup frameworks (OP Stack, Polygon CDK, etc). However, the Safe Sequencer needs to protect against an edge case where forced transactions from L1 are actually exploit transactions.
Forced Transactions
Rollups use forced transactions to inherit the censorship resistance of their L1. Users can submit a transaction on L1 and are guaranteed eventual inclusion into the rollup's chain. This forced transaction mechanism is appropriately called the "forced exit" or "escape hatch".
The Safe Sequencer needs to protect against the case where a forced transaction is an exploit and still maintain the properties of a rollup. To accomplish this, we introduce a configuration value that can toggle forced transactions and is managed by a censorship proof. Most importantly, if forced transactions are toggled off, an onchain proof that the Safe Sequencer is censoring can automatically turn forced transactions back on. This effectively turns the Safe Sequencer and it's deterministic, exploit detection algorithms into a rollup whose proof controls the forced transaction config value of another rollup rather than controlling funds in a bridge. 🤯
Each rollup framework that the Safe Sequencer integrates with must therefore have a forced transaction config value. We dive into the forced transaction config value addition to the OP Stack below.
OP Stack Integration
The OP Stack integration adds a forced transaction config value to the system contracts on L1 and L2. The cross domain messaging contracts are also modified to read this config value and act appropriately with respect to forced transactions from L1 -> L2.
One beautiful property of this design choice is that verifier nodes can use the vanilla OP Stack client if they want, since they are still just verifying the state-transition function of included transactions on their rollup. The contracts on L1 and L2 maintain state for the system's configuration and handle ensuring that forced transactions cannot cause an application exploit if forced replay is turned on.
Forced Replay
A system config value called "forced replay" is added to the L1 and L2 contracts. This config value is default off, meaning forced transactions operate as normal.
If forced replay is turned on:
- The
OptimismPortal
only accepts deposits from a single L1 contract. In most implementation options, this contract is theL1CrossDomainMessenger
. - The
L2CrossDomainMessenger
uses the above constraint to detect if arelayMessage
call is a forced transaction and pessimistically adds the message to it’s failed messages mapping to be replayed later, either through the sequencer or through a forced transaction if forced replay is subsequently turned off.
Off -> On Delay
The forced replay config value MUST be managed by an x-day delay before switching from off -> on.
On -> Off Censorship Proofs
The forced replay config value MUST be managed by the Safe Sequencer's exploit censorship proof and the customer rollup's governance.
UX Considerations
The sequencer SHOULD attempt to immediately replay any L1 → L2 message using it’s own gas balance.
Implementation
Config Value Update Flow
The system admin first calls the SystemConfig
contract on L1 to set the force replay boolean. The SystemConfig
contract then calls the OptimismPortal
which emits an event that is picked up
by rollup nodes and used to update the L1Block
contract on L2. This is the same standard system config update flow used throughout the OP Stack.
function setForceReplay(bool _forceReplay) external onlyOwner {
_setForceReplay(_forceReplay);
}
function _setForceReplay(bool _forceReplay) internal virtual {
// Set the force replay in storage and in the OptimismPortal.
ForceReplayL1L2Messages.set(_forceReplay);
OptimismPortal(payable(optimismPortal())).setForceReplay(_forceReplay);
}
/// @notice Sets the forced replay config value for the L2 system. Only the SystemConfig contract
/// can call this function.
function setForceReplay(bool _forceReplay) external {
if (msg.sender != address(systemConfig)) revert Unauthorized();
// Set L2 deposit gas as used without paying burning gas. Ensures that deposits cannot use too much L2 gas.
// This value must be large enough to cover the cost of calling `L1Block.setForceReplay`.
useGas(SYSTEM_DEPOSIT_GAS_LIMIT);
// Emit the special deposit transaction directly that sets the force replay value
// in the L1Block predeploy contract.
emit TransactionDeposited(
Constants.DEPOSITOR_ACCOUNT,
Predeploys.L1_BLOCK_ATTRIBUTES,
DEPOSIT_VERSION,
abi.encodePacked(
uint256(0), // mint
uint256(0), // value
uint64(SYSTEM_DEPOSIT_GAS_LIMIT), // gasLimit
false, // isCreation,
abi.encodeCall(L1Block.setForceReplay, (_forceReplay))
)
);
}
function setForceReplay(bool _forceReplay) external {
if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor();
ForceReplayL1L2Messages.set(_forceReplay);
emit ForceReplaySet(_forceReplay);
}
L1 -> L2 Message Passing
The OptimismPortal
is modified to check if the forced replay config value is turned on before emitting arbitrary TransactionDeposited
events.
function _depositTransaction(...)
...
if (isForcingReplay() && !(msg.sender == tx.origin && msg.sender == _to)) {
(address l1Messenger, address l2Messenger) = crossDomainMessengers();
require(msg.sender == l1Messenger);
require(_to == l2Messenger);
}
...
If the config value is turned on, then the deposit message must use the cross domain messengers or be guaranteed to be a simple ETH deposit from and to an EOA.
In the L2CrossDomainMessenger
we can now check if:
- The forced replay config value is on.
- The transaction is from the aliased
L1CrossDomainMessenger
(must be a forced transaction)
function relayMessage(...)
...
if (_relayMessageIsForcingReplay()) {
failedMessages[versionedHash] = true;
emit FailedRelayedMessage(versionedHash);
return;
}
...
The deposit message can now be replayed by anyone in the future and was not force executed on L2. We did all of this through a single config value read in the CrossDomainMessenger
and OptimismPortal
contracts without any changes to the derivation pipeline or fault proof program.
Next, we show how users can prove if a Safe Sequencer is operating maliciously and automatically fallback to the Ethereum-level censorship resistance of L1 forced transactions.