The rollup node is the component responsible for deriving the L2 chain from L1 blocks (and their associated receipts). This process happens in two steps:
- Read from L1 blocks and associated receipts, in order to generate payload attributes (essentially a block without output properties).
- Pass the payload attributes to the execution engine, so that output block properties may be computed.
While this process is conceptually a pure function from the L1 chain to the L2 chain, it is in practice incremental. The L2 chain is extended whenever new L1 blocks are added to the L1 chain. Similarly, the L2 chain re-organizes whenever the L1 chain re-organizes.
The part of the rollup node that derives the L2 chain is called the rollup driver. This document is currently only concerned with the specification of the rollup driver.
This section specifies how the rollup driver derives one L2 block per every L1 block. The L2 block will carry deposits, including a single L1 attributes deposit (which carries L1 block attributes) and zero or more transaction deposits submitted by user on L1.
The rollup reads the following data from each L1 block:
- L1 block attributes
- block number
- timestamp
- basefee
- random (the output of the
RANDOM
opcode)
- L1 log entries emitted for transaction deposits
A transaction deposit is an L2 transaction that has been submitted on L1, via a call to the deposit feed contract.
While deposits are notably (but not only) used to "deposit" (bridge) ETH and tokens to L2, the word deposit should be understood as "a transaction deposited to L2".
The L1 attributes are read from the L1 block header, while deposits are read from the block's receipts. Refer to the deposit feed contract specification for details on how deposits are encoded as log entries.
From the data read from L1, the rollup node constructs an expanded version of the Engine API PayloadAttributesV1
object, which includes an additional transactions
field:
PayloadAttributesOPV1: {
timestamp: QUANTITY
random: DATA (32 bytes)
suggestedFeeRecipient: DATA (20 bytes)
transactions: array of DATA
}
The type notation used here refers to the HEX value encoding used by the Ethereum JSON-RPC API
specification, as this structure will need to be sent over JSON-RPC. array
refers to a JSON array.
The object properties must be set as follows:
timestamp
is set to the current unix time (number of elapsed seconds since 00:00:00 UTC on 1 January 1970), rounded to the closest multiple of 2 seconds. No two blocks may have the same timestamp.random
is set to the random L1 block attributesuggestedFeeRecipient
is set to an address where the sequencer would like to direct the feestransactions
is an array containing the L1 attributes deposit as well as transaction deposits, whose format is specified in the next section.
The transactions
array is filled with user-submitted deposits, prefixed by the (single) L1 attributes deposit. The
format for deposits is described at the top of the deposit specification.
The rollup node is responsible for encoding the L1 attributes deposit based on the attributes (block number, timestamp
and basefee) of the L1 block, as specified in the L1 Attributes Deposit section of the
deposit specification. It must also encode the transaction deposits based on the TransactionDeposited
event
emitted by the deposit contract, as specified by the L1 Transaction Deposits section of the
same document.
Here is an example valid PayloadAttributesOPV1
object, which contains an L1 attributes deposit as well as a single
transaction deposit:
{
timestamp: "0x61a6336f",
random: "0xde5dff2b0982ecbbd38081eb8f4aed0525140dc1c1d56f995b4fa801a3f2649e",
suggestedFeeRecipient: "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
transactions: [
"TODO generate abi-encoded L1 attribute deposit",
"0x02f87101058459682f0085199c82cc0082520894ab5801a7d398351b8be11c439e05c5b3259aec9b8609184e72a00080c080a0a6d217a91ea344fc09f740f104f764d71bb1ca9a8e159117d2d27091ea5fce91a04cf5add5f5b7d791a2c4663ab488cb581df800fe0910aa755099ba466b49fd69"
]
}
The Optimism execution engine is specified in the Execution Engine Specification.
This section defines how the rollup driver must interact with the execution engine's in order to convert payload attributes into L2 blocks.
TODO This section probably includes too much redundant details that will need to be removed once the execution engine spec is up.
Optimism's execution engine API is built upon Ethereum's Engine API specification, with a couple of modifications. That specification builds upon Ethereum's JSON-RPC API specification, which itself builds upon the JSON-RPC specification.
In particular, the Ethereum's Engine API specification specifies a JSON-RPC endpoint with a number of JSON-RPC routes, which are the means through which the rollup driver interacts with the execution engine.
Instead of calling engine_forkchoiceUpdatedV1
, the rollup driver must call the new engine_forkchoiceUpdatedOPV1
route. This has the same signature, except that:
-
it takes a
PayloadAttributesOPV1
object as input instead ofPayloadAttributesV1
. The execution engine must include the valid transactions supplied in this object in the block, in the same order as they were supplied, and only those. See the previous section for the specification of how the properties must be set. -
we repurpose the
ForkchoiceStateV1
structure with the following property semantics:headBlockHash
: block hash of the last block of the L2 chain, according to the rollup driver.safeBlockHash
: same asheadBlockHash
.finalizedBlockHash
: the hash of the block whose number isnumber(headBlockHash) - FINALIZATION_DELAY_BLOCKS
if the number of that block is>= L2_CHAIN_INCEPTION
, 0 otherwise (whereFINALIZATION_DELAY_BLOCKS
= 50400, approximately 7 days worth of L1 blocks) andL2_CHAIN_INCEPTION
is the L2 chain inception (the number of the first L1 block for which an L2 block was produced). See the Finalization Guarantees section for more details.
Note: the properties of
ForkchoiceStateV1
can be used to anchor queries to the regular (non-engine-API) JSON-RPC endpoint of the execution engine. See here for more information.
TODO LINK L2 JSON RPC API (might be the same as L1's)
The payloadID
returned by engine_forkchoiceUpdatedOPV1
can then be passed to engine_getPayloadV1
in order to
obtain an ExecutionPayloadV1
, which fully defines a new L2 block.
The rollup driver must then instruct the execution engine to execute the block by calling engine_executePayloadV1
.
This returns the new L2 block hash.
All invocations of engine_forkchoiceUpdatedOPV1
, engine_getPayloadV1
and engine_executePayloadV1
by the
rollup driver should not result in errors assuming conformity with the specification. Said otherwise, all errors are
implementation concerns and it is up to them to handle them (e.g. by retrying, or by stopping the chain derivation and
requiring manual user intervention).
The following scenarios are assimilated to errors:
engine_forkchoiceUpdatedOPV1
returning astatus
of"SYNCING"
instead of"SUCCESS"
whenever passed aheadBlockHash
that it retrieved from a previous call toengine_executePayloadV1
.engine_executePayloadV1
returning astatus
of"SYNCING"
or"INVALID"
whenever passed an execution payload that was obtained by a previous call toengine_getPayloadV1
.
The previous section on L2 chain derivation assumes linear progression of the L1 chain. It is also applicable for batch processing, meaning that any given point in time, the canonical L2 chain is given by processing the whole L1 chain since the L2 chain inception.
By itself, the previous section fully specifies the behaviour of the rollup driver. The current section is non-specificative but shows how L1 re-orgs can be handled in practice.
In practice, the L1 chain is processed incrementally. However, the L1 chain may occasionally re-organize, meaning the head of the L1 chain changes to a block that is not the child of the previous head but rather one of its "cousins" (i.e. the descendant of an ancestor of the previous head). In those case, the rollup driver must:
- Call
engine_forkchoiceUpdatedOPV1
for the new L2 chain head- Pass
null
for thepayloadAttributes
parameter. - Fill the
ForkchoiceStateV1
object according to the section on the execution engine, but setheadBlockHash
to the hash of the new L2 chain head.safeBlockHash
andfinalizedBlockHash
must be updated accordingly.
- Pass
- If the call returns
"SUCCESS"
, we are done: the execution engine retrieved all the new L2 blocks via block sync. - Otherwise the call returns
"SYNCING"
, and we must derive the new blocks ourselves. Start by locating the common ancestor, a block that is an ancestor of both the previous and new head. - Isolate the range of L1 blocks from
common ancestor
(excluded) tonew head
(included). - For each such block, call
engine_forkchoiceUpdatedOPV1
,engine_getPayloadV1
, andengine_executePayloadV1
.- Fill the
PayloadAttributesOPV1
object according to the section on payload attributes. - Fill the
ForkchoiceStateV1
object according to the section on the execution engine, but setheadBlockHash
to the hash of the last processed L2 block (use the hash of the common ancestor initially) instead of the last L2 chain head.safeBlockHash
andfinalizedBlockHash
must be updated accordingly.
- Fill the
Note that post-merge, the L1 chain will offer finalization guarantees meaning that it won't be able to re-org more than
FINALIZATION_DELAY_BLOCKS == 50400
in the past, hence preserving our finalization guarantees.
Just like before, the meaning of errors returned by RPC calls is unspecified and must be handled at the implementer's discretion, while remaining compatible with the specification.
As already alluded to in the section on interacting with the execution engine, an L2 block is
considered finalized after a delay of FINALIZATION_DELAY_BLOCKS == 50400
blocks after the L1 block that generated
it. This is a duration of approximately 7 days worth of L1 blocks.
L1 Ethereum reaches finality approximately every 12 minutes, so these L2 blocks can safely be considered to be final: they will never disappear from the chain's history because of a re-org.