Commit 731bad4e authored by Andreas Bigger's avatar Andreas Bigger

Merge branch 'develop' into refcell/bindings

parents 6755bfc2 e768427b
...@@ -14,12 +14,12 @@ import ( ...@@ -14,12 +14,12 @@ import (
// version 1 messages have a value and the most significant // version 1 messages have a value and the most significant
// byte of the nonce is a 1 // byte of the nonce is a 1
type CrossDomainMessage struct { type CrossDomainMessage struct {
Nonce *big.Int Nonce *big.Int `json:"nonce"`
Sender *common.Address Sender *common.Address `json:"sender"`
Target *common.Address Target *common.Address `json:"target"`
Value *big.Int Value *big.Int `json:"value"`
GasLimit *big.Int GasLimit *big.Int `json:"gasLimit"`
Data []byte Data []byte `json:"data"`
} }
// NewCrossDomainMessage creates a CrossDomainMessage. // NewCrossDomainMessage creates a CrossDomainMessage.
......
...@@ -76,7 +76,7 @@ func MigrateWithdrawal(withdrawal *LegacyWithdrawal, l1CrossDomainMessenger *com ...@@ -76,7 +76,7 @@ func MigrateWithdrawal(withdrawal *LegacyWithdrawal, l1CrossDomainMessenger *com
withdrawal.Target, withdrawal.Target,
value, value,
new(big.Int), new(big.Int),
withdrawal.Data, []byte(withdrawal.Data),
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot abi encode relayMessage: %w", err) return nil, fmt.Errorf("cannot abi encode relayMessage: %w", err)
......
...@@ -2,11 +2,13 @@ package crossdomain ...@@ -2,11 +2,13 @@ package crossdomain
import ( import (
"errors" "errors"
"fmt"
"math/big" "math/big"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
) )
...@@ -28,7 +30,7 @@ type Withdrawal struct { ...@@ -28,7 +30,7 @@ type Withdrawal struct {
Target *common.Address `json:"target"` Target *common.Address `json:"target"`
Value *big.Int `json:"value"` Value *big.Int `json:"value"`
GasLimit *big.Int `json:"gasLimit"` GasLimit *big.Int `json:"gasLimit"`
Data []byte `json:"data"` Data hexutil.Bytes `json:"data"`
} }
// NewWithdrawal will create a Withdrawal // NewWithdrawal will create a Withdrawal
...@@ -44,7 +46,7 @@ func NewWithdrawal( ...@@ -44,7 +46,7 @@ func NewWithdrawal(
Target: target, Target: target,
Value: value, Value: value,
GasLimit: gasLimit, GasLimit: gasLimit,
Data: data, Data: hexutil.Bytes(data),
} }
} }
...@@ -58,9 +60,9 @@ func (w *Withdrawal) Encode() ([]byte, error) { ...@@ -58,9 +60,9 @@ func (w *Withdrawal) Encode() ([]byte, error) {
{Name: "gasLimit", Type: Uint256Type}, {Name: "gasLimit", Type: Uint256Type},
{Name: "data", Type: BytesType}, {Name: "data", Type: BytesType},
} }
enc, err := args.Pack(w.Nonce, w.Sender, w.Target, w.Value, w.GasLimit, w.Data) enc, err := args.Pack(w.Nonce, w.Sender, w.Target, w.Value, w.GasLimit, []byte(w.Data))
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("cannot encode withdrawal: %w", err)
} }
return enc, nil return enc, nil
} }
...@@ -110,7 +112,7 @@ func (w *Withdrawal) Decode(data []byte) error { ...@@ -110,7 +112,7 @@ func (w *Withdrawal) Decode(data []byte) error {
w.Target = &target w.Target = &target
w.Value = value w.Value = value
w.GasLimit = gasLimit w.GasLimit = gasLimit
w.Data = msgData w.Data = hexutil.Bytes(msgData)
return nil return nil
} }
...@@ -150,6 +152,6 @@ func (w *Withdrawal) WithdrawalTransaction() bindings.TypesWithdrawalTransaction ...@@ -150,6 +152,6 @@ func (w *Withdrawal) WithdrawalTransaction() bindings.TypesWithdrawalTransaction
Target: *w.Target, Target: *w.Target,
Value: w.Value, Value: w.Value,
GasLimit: w.GasLimit, GasLimit: w.GasLimit,
Data: w.Data, Data: []byte(w.Data),
} }
} }
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
[g-payload-attr]: glossary.md#payload-attributes [g-payload-attr]: glossary.md#payload-attributes
[g-block]: glossary.md#block [g-block]: glossary.md#block
[g-exec-engine]: glossary.md#execution-engine [g-exec-engine]: glossary.md#execution-engine
[g-reorg]: glossary.md#re-organization [g-reorg]: glossary.md#chain-re-organization
[g-receipts]: glossary.md#receipt [g-receipts]: glossary.md#receipt
[g-inception]: glossary.md#L2-chain-inception [g-inception]: glossary.md#L2-chain-inception
[g-deposit-contract]: glossary.md#deposit-contract [g-deposit-contract]: glossary.md#deposit-contract
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
[g-batcher-transaction]: glossary.md#batcher-transaction [g-batcher-transaction]: glossary.md#batcher-transaction
[g-avail-provider]: glossary.md#data-availability-provider [g-avail-provider]: glossary.md#data-availability-provider
[g-batcher]: glossary.md#batcher [g-batcher]: glossary.md#batcher
[g-l2-output]: glossary.md#l2-output [g-l2-output]: glossary.md#l2-output-root
[g-fault-proof]: glossary.md#fault-proof [g-fault-proof]: glossary.md#fault-proof
[g-channel]: glossary.md#channel [g-channel]: glossary.md#channel
[g-channel-frame]: glossary.md#channel-frame [g-channel-frame]: glossary.md#channel-frame
...@@ -69,30 +69,29 @@ ...@@ -69,30 +69,29 @@
- [Timeouts](#timeouts) - [Timeouts](#timeouts)
- [Reading](#reading) - [Reading](#reading)
- [Loading frames](#loading-frames) - [Loading frames](#loading-frames)
- [Batch Decoding](#batch-decoding) - [Channel Reader (Batch Decoding)](#channel-reader-batch-decoding)
- [Batch Buffering](#batch-buffering) - [Batch Queue](#batch-queue)
- [Payload Attributes Derivation](#payload-attributes-derivation) - [Payload Attributes Derivation](#payload-attributes-derivation)
- [Engine Queue](#engine-queue) - [Engine Queue](#engine-queue)
- [Engine API usage](#engine-api-usage)
- [Forkchoice synchronization](#forkchoice-synchronization)
- [L1-consolidation: payload attributes matching](#l1-consolidation-payload-attributes-matching)
- [L1-sync: payload attributes processing](#l1-sync-payload-attributes-processing)
- [Processing unsafe payload attributes](#processing-unsafe-payload-attributes)
- [Resetting the Pipeline](#resetting-the-pipeline) - [Resetting the Pipeline](#resetting-the-pipeline)
- [Finding the sync starting point](#finding-the-sync-starting-point)
- [Resetting derivation stages](#resetting-derivation-stages)
- [About reorgs Post-Merge](#about-reorgs-post-merge)
- [Deriving Payload Attributes](#deriving-payload-attributes) - [Deriving Payload Attributes](#deriving-payload-attributes)
- [Deriving the Transaction List](#deriving-the-transaction-list) - [Deriving the Transaction List](#deriving-the-transaction-list)
- [Building Individual Payload Attributes](#building-individual-payload-attributes) - [Building Individual Payload Attributes](#building-individual-payload-attributes)
- [Communication with the Execution Engine](#communication-with-the-execution-engine)
- [WARNING: BELOW THIS LINE, THE SPEC HAS NOT BEEN REVIEWED AND MAY CONTAIN MISTAKES](#warning-below-this-line-the-spec-has-not-been-reviewed-and-may-contain-mistakes)
- [Handling L1 Re-Orgs](#handling-l1-re-orgs)
- [Resetting the Engine Queue](#resetting-the-engine-queue)
- [Resetting Payload Attribute Derivation](#resetting-payload-attribute-derivation)
- [Resetting Batch Decoding](#resetting-batch-decoding)
- [Resetting Channel Buffering](#resetting-channel-buffering)
- [Resetting L1 Retrieval & L1 Traversal](#resetting-l1-retrieval--l1-traversal)
- [Reorgs Post-Merge](#reorgs-post-merge)
<!-- END doctoc generated TOC please keep comment here to allow auto update --> <!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Overview # Overview
> **Note** the following assumes a single sequencer and batcher. In the future, the design will be adapted to > **Note** the following assumes a single sequencer and batcher. In the future, the design will be adapted to
> accomodate multiple such entities. > accommodate multiple such entities.
[L2 chain derivation][g-derivation] — deriving L2 [blocks][g-block] from L1 data — is one of the main responsibility of [L2 chain derivation][g-derivation] — deriving L2 [blocks][g-block] from L1 data — is one of the main responsibility of
the [rollup node][g-rollup-node], both in validator mode, and in sequencer mode (where derivation acts as a sanity check the [rollup node][g-rollup-node], both in validator mode, and in sequencer mode (where derivation acts as a sanity check
...@@ -105,7 +104,7 @@ L1 block number. ...@@ -105,7 +104,7 @@ L1 block number.
To derive the L2 blocks in an epoch `E`, we need the following inputs: To derive the L2 blocks in an epoch `E`, we need the following inputs:
- The L1 [sequencing window][g-sequencing-window] for epoch `E`: the L1 blocks in the range `[E, E + SWS)` where `SWS` - The L1 [sequencing window][g-sequencing-window] for epoch `E`: the L1 blocks in the range `[E, E + SWS)` where `SWS`
is the sequencing window size (note that this means that epochs are overlapping). In particular we need: is the sequencing window size (note that this means that epochs are overlapping). In particular, we need:
- The [batcher transactions][g-batcher-transaction] included in the sequencing window. These allow us to - The [batcher transactions][g-batcher-transaction] included in the sequencing window. These allow us to
reconstruct [sequencer batches][g-sequencer-batch] containing the transactions to include in L2 blocks (each batch reconstruct [sequencer batches][g-sequencer-batch] containing the transactions to include in L2 blocks (each batch
maps to a single L2 block). maps to a single L2 block).
...@@ -118,12 +117,10 @@ To derive the L2 blocks in an epoch `E`, we need the following inputs: ...@@ -118,12 +117,10 @@ To derive the L2 blocks in an epoch `E`, we need the following inputs:
[L2 genesis state][g-l2-genesis]. [L2 genesis state][g-l2-genesis].
- An epoch `E` does not exist if `E <= L2CI`, where `L2CI` is the [L2 chain inception][g-l2-chain-inception]. - An epoch `E` does not exist if `E <= L2CI`, where `L2CI` is the [L2 chain inception][g-l2-chain-inception].
> **TODO** specify sequencing window size (current thinking: on the order of a few hours, to give maximal flexibility to
> the batch submitter)
To derive the whole L2 chain from scratch, we simply start with the [L2 genesis state][g-l2-genesis], and the [L2 chain To derive the whole L2 chain from scratch, we simply start with the [L2 genesis state][g-l2-genesis], and the [L2 chain
inception][g-l2-chain-inception] as first epoch, then process all sequencing windows in order. Refer to the inception][g-l2-chain-inception] as first epoch, then process all sequencing windows in order. Refer to the
[Architecture section][architecture] for more information on how we implement this in practice. [Architecture section][architecture] for more information on how we implement this in practice.
The L2 chain may contain pre-Bedrock history, but the L2 genesis here refers to the first Bedrock L2 block.
Each epoch may contain a variable number of L2 blocks (one every `l2_block_time`, 2s on Optimism), at the discretion of Each epoch may contain a variable number of L2 blocks (one every `l2_block_time`, 2s on Optimism), at the discretion of
[the sequencer][g-sequencer], but subject to the following constraints for each block: [the sequencer][g-sequencer], but subject to the following constraints for each block:
...@@ -139,30 +136,28 @@ Each epoch may contain a variable number of L2 blocks (one every `l2_block_time` ...@@ -139,30 +136,28 @@ Each epoch may contain a variable number of L2 blocks (one every `l2_block_time`
- `l1_timestamp` is the timestamp of the L1 block associated with the L2 block's epoch - `l1_timestamp` is the timestamp of the L1 block associated with the L2 block's epoch
- `max_sequencer_drift` is the most a sequencer is allowed to get ahead of L1 - `max_sequencer_drift` is the most a sequencer is allowed to get ahead of L1
> **TODO** specify max sequencer drift (current thinking: on the order of 10
> minutes, we've been using 2-4 minutes in testnets)
Put together, these constraints mean that there must be an L2 block every `l2_block_time` seconds, and that the Put together, these constraints mean that there must be an L2 block every `l2_block_time` seconds, and that the
timestamp for the first L2 block of an epoch must never fall behind the timestamp of the L1 block matching the epoch. timestamp for the first L2 block of an epoch must never fall behind the timestamp of the L1 block matching the epoch.
Post-merge, Ethereum has a fixed [block time][g-block-time] of 12s (though some slots can be skipped). It is thus Post-merge, Ethereum has a fixed [block time][g-block-time] of 12s (though some slots can be skipped). It is thus
expected that, most of the time, each epoch on Optimism will contain `12/2 = 6` L2 blocks. The sequencer can however expected that with a 2-second L2 block time, most of the time, each epoch will contain `12/2 = 6` L2 blocks.
lengthen or shorten epochs (subject to above constraints). The rationale is to maintain liveness in case of either a The sequencer can however lengthen or shorten epochs (subject to above constraints).
skipped slot on L1, or a temporary loss of connection to L1 — which requires longer epochs. Shorter epochs are then The rationale is to maintain liveness in case of either a skipped slot on L1, or a temporary loss of connection to L1 —
required to avoid L2 timestamps drifting further and further ahead of L1. which requires longer epochs.
Shorter epochs are then required to avoid L2 timestamps drifting further and further ahead of L1.
## Eager Block Derivation ## Eager Block Derivation
In practice, it is often not necesary to wait for a full sequencing window of L1 blocks in order to start deriving the In practice, it is often not necessary to wait for a full sequencing window of L1 blocks in order to start deriving the
L2 blocks in an epoch. Indeed, as long as we are able to reconstruct sequential batches, we can start deriving the L2 blocks in an epoch. Indeed, as long as we are able to reconstruct sequential batches, we can start deriving the
corresponding L2 blocks. We call this *eager block derivation*. corresponding L2 blocks. We call this *eager block derivation*.
However, in the very worst case, we can only reconstruct the batch for the first L2 block in the epoch by reading the However, in the very worst case, we can only reconstruct the batch for the first L2 block in the epoch by reading the
last L1 block of the sequencing window. This happens when some data for that batch is included in the last L1 block of last L1 block of the sequencing window. This happens when some data for that batch is included in the last L1 block of
the window. In that case, not only can we not derive the first L2 block in the poch, we also can't derive any further L2 the window. In that case, not only can we not derive the first L2 block in the epoch, we also cannot derive any further
block in the epoch until then, as they need the state that results from applying the epoch's first L2 block. (Note that L2 block in the epoch until then, as they need the state that results from applying the epoch's first L2 block.
this only applies to *block* derivation. We can still derive further batches, we just won't be able to create blocks (Note that this only applies to *block* derivation. Batches can still be derived and tentatively queued,
from them.) we just won't be able to create blocks from them.)
------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------
...@@ -182,8 +177,8 @@ reference to the previous block (\*). ...@@ -182,8 +177,8 @@ reference to the previous block (\*).
(\*) This matters in some edge case where a L1 reorg would occur and a batch would be reposted to the L1 chain but not (\*) This matters in some edge case where a L1 reorg would occur and a batch would be reposted to the L1 chain but not
the preceding batch, whereas the predecessor of an L2 block cannot possibly change. the preceding batch, whereas the predecessor of an L2 block cannot possibly change.
This means that even if the sequencer applies a state transition incorrectly, the transactions in the batch will stil be This means that even if the sequencer applies a state transition incorrectly, the transactions in the batch will still
considered part of the canonical L2 chain. Batches are still subject to validity checks (i.e. they have to be encoded be considered part of the canonical L2 chain. Batches are still subject to validity checks (i.e. they have to be encoded
correctly), and so are individual transactions within the batch (e.g. signatures have to be valid). Invalid batches and correctly), and so are individual transactions within the batch (e.g. signatures have to be valid). Invalid batches and
invalid individual transactions within an otherwise valid batch are discarded by correct nodes. invalid individual transactions within an otherwise valid batch are discarded by correct nodes.
...@@ -195,17 +190,6 @@ Refer to the [Batch Submission specification][batcher-spec] for more information ...@@ -195,17 +190,6 @@ Refer to the [Batch Submission specification][batcher-spec] for more information
[batcher-spec]: batcher.md [batcher-spec]: batcher.md
> **TODO** rewrite the batch submission specification
>
> Here are some things that should be included there:
>
> - There may be different concurrent data submissions to L1
> - There may be different actors that submit the data, the system cannot rely on a single EOA nonce value.
> - The batcher requests safe L2 safe head from the rollup node, then queries the execution engine for the block data.
> - In the future we might be able to get the safe hea dinformation from the execution engine directly. Not possible
> right now but there is an upstream geth PR open.
> - specify batcher authentication (cf. TODO below)
## Batch Submission Wire Format ## Batch Submission Wire Format
[wire-format]: #batch-submission-wire-format [wire-format]: #batch-submission-wire-format
...@@ -217,7 +201,7 @@ The [batcher][g-batcher] submits [batcher transactions][g-batcher-transaction] t ...@@ -217,7 +201,7 @@ The [batcher][g-batcher] submits [batcher transactions][g-batcher-transaction] t
provider][g-avail-provider]. These transactions contain one or multiple [channel frames][g-channel-frame], which are provider][g-avail-provider]. These transactions contain one or multiple [channel frames][g-channel-frame], which are
chunks of data belonging to a [channel][g-channel]. chunks of data belonging to a [channel][g-channel].
A [channel][g-channel] is a sequence of [sequencer batches][g-sequencer-batch] (for sequential blocks) compressed A [channel][g-channel] is a sequence of [sequencer batches][g-sequencer-batch] (for any L2 blocks) compressed
together. The reason to group multiple batches together is simply to obtain a better compression rate, hence reducing together. The reason to group multiple batches together is simply to obtain a better compression rate, hence reducing
data availability costs. data availability costs.
...@@ -227,15 +211,20 @@ into chunks known as [channel frames][g-channel-frame]. A single batcher transac ...@@ -227,15 +211,20 @@ into chunks known as [channel frames][g-channel-frame]. A single batcher transac
This design gives use the maximum flexibility in how we aggregate batches into channels, and split channels over batcher This design gives use the maximum flexibility in how we aggregate batches into channels, and split channels over batcher
transactions. It notably allows us to maximize data utilisation in a batcher transaction: for instance it allows us to transactions. It notably allows us to maximize data utilisation in a batcher transaction: for instance it allows us to
pack the final (small) frame of a window with large frames from the next window. It also allows the [batcher][g-batcher] pack the final (small) frame of a window with large frames from the next window.
to employ multiple signers (private keys) to submit one or multiple channels in parallel (1).
(1) This helps alleviate issues where, because of transaction nonces, multiple transactions made by the same signer are In the future this channel identification feature also allows the [batcher][g-batcher] to employ multiple signers
stuck waiting on the inclusion of a previous transaction. (private keys) to submit one or multiple channels in parallel (1).
(1) This helps alleviate issues where, because of transaction nonce values affecting the L2 tx-pool and thus inclusion:
multiple transactions made by the same signer are stuck waiting on the inclusion of a previous transaction.
Also note that we use a streaming compression scheme, and we do not need to know how many blocks a channel will end up Also note that we use a streaming compression scheme, and we do not need to know how many blocks a channel will end up
containing when we start a channel, or even as we send the first frames in the channel. containing when we start a channel, or even as we send the first frames in the channel.
And by splitting channels across multiple data transactions, the L2 can have larger block data than the
data-availability layer may support.
All of this is illustrated in the following diagram. Explanations below. All of this is illustrated in the following diagram. Explanations below.
![batch derivation chain diagram](./assets/batch-deriv-chain.svg) ![batch derivation chain diagram](./assets/batch-deriv-chain.svg)
...@@ -253,7 +242,7 @@ Each colored chunk within the boxes represents a [channel frame][g-channel-frame ...@@ -253,7 +242,7 @@ Each colored chunk within the boxes represents a [channel frame][g-channel-frame
In the next line, the rounded boxes represent individual [sequencer batches][g-sequencer-batch] that were extracted from In the next line, the rounded boxes represent individual [sequencer batches][g-sequencer-batch] that were extracted from
the channels. The four blue/purple/pink were derived from channel `A` while the other were derived from channel `B`. the channels. The four blue/purple/pink were derived from channel `A` while the other were derived from channel `B`.
These batches are here represented in the order the were decoded from batches (in this case `B` is decoded first). These batches are here represented in the order they were decoded from batches (in this case `B` is decoded first).
> **Note** The caption here says "Channel B was seen first and will be decoded into batches first", but this is not a > **Note** The caption here says "Channel B was seen first and will be decoded into batches first", but this is not a
> requirement. For instance, it would be equally acceptable for an implementation to peek into the channels and decode > requirement. For instance, it would be equally acceptable for an implementation to peek into the channels and decode
...@@ -278,12 +267,12 @@ information about the L1 block that matches the L2 block's epoch. The first numb ...@@ -278,12 +267,12 @@ information about the L1 block that matches the L2 block's epoch. The first numb
the second number (the "sequence number") denotes the position within the epoch. the second number (the "sequence number") denotes the position within the epoch.
Finally, the sixth line shows [user-deposited transactions][g-user-deposited] derived from the [deposit Finally, the sixth line shows [user-deposited transactions][g-user-deposited] derived from the [deposit
contract][g-deposit-contract] event mentionned earlier. contract][g-deposit-contract] event mentioned earlier.
Note the `101-0` L1 attributes transaction on the bottom right of the diagram. Its presence there is only possible if Note the `101-0` L1 attributes transaction on the bottom right of the diagram. Its presence there is only possible if
frame `B2` indicates that it is the last frame within the channel and (2) no empty blocks must be inserted. frame `B2` indicates that it is the last frame within the channel and (2) no empty blocks must be inserted.
The diagram does not specify the sequencing window size in use, but from it we can infer that it must be at least 4 The diagram does not specify the sequencing window size in use, but from this we can infer that it must be at least 4
blocks, because the last frame of channel `A` appears in block 102, but belong to epoch 99. blocks, because the last frame of channel `A` appears in block 102, but belong to epoch 99.
As for the comment on "security types", it explains the classification of blocks as used on L1 and L2. As for the comment on "security types", it explains the classification of blocks as used on L1 and L2.
...@@ -295,8 +284,7 @@ As for the comment on "security types", it explains the classification of blocks ...@@ -295,8 +284,7 @@ As for the comment on "security types", it explains the classification of blocks
than the [challenge period]. than the [challenge period].
These security levels map to the `headBlockHash`, `safeBlockHash` and `finalizedBlockHash` values transmitted when These security levels map to the `headBlockHash`, `safeBlockHash` and `finalizedBlockHash` values transmitted when
interacting with the [execution-engine API][exec-engine]. Refer to the the [Communication with the Execution interacting with the [execution-engine API][exec-engine].
Engine][exec-engine-comm] section for more information.
### Batcher Transaction Format ### Batcher Transaction Format
...@@ -391,7 +379,7 @@ Recall that a batch contains a list of transactions to be included in a specific ...@@ -391,7 +379,7 @@ Recall that a batch contains a list of transactions to be included in a specific
A batch is encoded as `batch_version ++ content`, where `content` depends on the `batch_version`: A batch is encoded as `batch_version ++ content`, where `content` depends on the `batch_version`:
| `batch_version` | `content` | | `batch_version` | `content` |
| --------------- |------------------------------------------------------------------------------------| |-----------------|------------------------------------------------------------------------------------|
| 0 | `rlp_encode([parent_hash, epoch_number, epoch_hash, timestamp, transaction_list])` | | 0 | `rlp_encode([parent_hash, epoch_number, epoch_hash, timestamp, transaction_list])` |
where: where:
...@@ -410,8 +398,8 @@ where: ...@@ -410,8 +398,8 @@ where:
Unknown versions make the batch invalid (it must be ignored by the rollup node), as do malformed contents. Unknown versions make the batch invalid (it must be ignored by the rollup node), as do malformed contents.
The `epoch_number` and the `timestamp` must also respect the constraints listed in the [Batch The `epoch_number` and the `timestamp` must also respect the constraints listed in the [Batch Queue][batch-queue]
Buffering][batch-buffering] section, otherwise the batch is considered invalid. section, otherwise the batch is considered invalid and will be ignored.
------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------
...@@ -419,15 +407,12 @@ Buffering][batch-buffering] section, otherwise the batch is considered invalid. ...@@ -419,15 +407,12 @@ Buffering][batch-buffering] section, otherwise the batch is considered invalid.
[architecture]: #architecture [architecture]: #architecture
The above describes the general process of L2 chain derivation, and specifies how batches are encoded within [batcher The above primarily describes the general encodings used in L2 chain derivation,
transactions][g-batcher-transaction]. primarily how batches are encoded within [batcher transactions][g-batcher-transaction].
However, there remains many details to specify. These are mostly tied to the rollup node architecture for derivation. This section describes how the L2 chain is produced from the L1 batches using a pipeline architecture.
Therefore we present this architecture as a way to specify these details.
A validator that only reads from L1 (and so doesn't interact with the sequencer directly) does not need to be A verifier may implement this differently, but must be semantically equivalent to not diverge from the L2 chain.
implemented in the way presented below. It does however need to derive the same blocks (i.e. it needs to be semantically
equivalent). We do believe the architecture presented below has many advantages.
## L2 Chain Derivation Pipeline ## L2 Chain Derivation Pipeline
...@@ -437,16 +422,15 @@ Our architecture decomposes the derivation process into a pipeline made up of th ...@@ -437,16 +422,15 @@ Our architecture decomposes the derivation process into a pipeline made up of th
1. L1 Traversal 1. L1 Traversal
2. L1 Retrieval 2. L1 Retrieval
3. Channel Bank 3. Frame Queue
4. Batch Decoding (called `ChannelInReader` in the code) 4. Channel Bank
5. Batch Buffering (Called `BatchQueue` in the code) 5. Channel Reader (Batch Decoding)
6. Payload Attributes Derivation (called `AttributesQueue` in the code) 6. Batch Queue
7. Engine Queue 7. Payload Attributes Derivation
8. Engine Queue
> **TODO** can we change code names for these three things? maybe as part of a refactor The data flows from the start (outer) of the pipeline towards the end (inner).
From the innermost stage the data is pulled from the outermost stage.
The data flows flows from the start (outer) of the pipeline towards the end (inner). Each stage is able to push data to
the next stage.
However, data is *processed* in reverse order. Meaning that if there is any data to be processed in the last stage, it However, data is *processed* in reverse order. Meaning that if there is any data to be processed in the last stage, it
will be processed first. Processing proceeds in "steps" that can be taken at each stage. We try to take as many steps as will be processed first. Processing proceeds in "steps" that can be taken at each stage. We try to take as many steps as
...@@ -455,9 +439,11 @@ possible in the last (most inner) stage before taking any steps in its outer sta ...@@ -455,9 +439,11 @@ possible in the last (most inner) stage before taking any steps in its outer sta
This ensures that we use the data we already have before pulling more data and minimizes the latency of data traversing This ensures that we use the data we already have before pulling more data and minimizes the latency of data traversing
the derivation pipeline. the derivation pipeline.
Each stage can maintain its own inner state as necessary. **In particular, each stage maintains a L1 block reference Each stage can maintain its own inner state as necessary. In particular, each stage maintains a L1 block reference
(number + hash) to the latest L1 block such that all data originating from previous blocks has been fully processed, and (number + hash) to the latest L1 block such that all data originating from previous blocks has been fully processed, and
the data from that block is being or has been processed.** the data from that block is being or has been processed. This allows the innermost stage to account for finalization of
the L1 data-availability used to produce the L2 chain, to reflect in the L2 chain forkchoice when the L2 chain inputs
become irreversible.
Let's briefly describe each stage of the pipeline. Let's briefly describe each stage of the pipeline.
...@@ -547,16 +533,16 @@ Frame insertion conditions: ...@@ -547,16 +533,16 @@ Frame insertion conditions:
If a frame is closing (`is_last == 1`) any existing higher-numbered frames are removed from the channel. If a frame is closing (`is_last == 1`) any existing higher-numbered frames are removed from the channel.
### Batch Decoding ### Channel Reader (Batch Decoding)
In the *Batch Decoding* stage, we decompress the channel we received in the last stage, then parse In this stage, we decompress the channel we pull from the last stage, and then parse
[batches][g-sequencer-batch] from the decompressed byte stream. [batches][g-sequencer-batch] from the decompressed byte stream.
See [Batch Format][batch-format] for decompression and decoding specification. See [Batch Format][batch-format] for decompression and decoding specification.
### Batch Buffering ### Batch Queue
[batch-buffering]: #batch-buffering [batch-queue]: #batch-queue
During the *Batch Buffering* stage, we reorder batches by their timestamps. If batches are missing for some [time During the *Batch Buffering* stage, we reorder batches by their timestamps. If batches are missing for some [time
slots][g-time-slot] and a valid batch with a higher timestamp exists, this stage also generates empty batches to fill slots][g-time-slot] and a valid batch with a higher timestamp exists, this stage also generates empty batches to fill
...@@ -578,6 +564,7 @@ A batch can have 4 different forms of validity: ...@@ -578,6 +564,7 @@ A batch can have 4 different forms of validity:
- `future`: the batch may be valid, but cannot be processed yet and should be checked again later. - `future`: the batch may be valid, but cannot be processed yet and should be checked again later.
The batches are processed in order of the inclusion on L1: if multiple batches can be `accept`-ed the first is applied. The batches are processed in order of the inclusion on L1: if multiple batches can be `accept`-ed the first is applied.
An implementation can defer `future` batches a later derivation step to reduce validation work.
The batches validity is derived as follows: The batches validity is derived as follows:
...@@ -649,32 +636,83 @@ The system configuration is updated with L1 log events whenever the L1 epoch ref ...@@ -649,32 +636,83 @@ The system configuration is updated with L1 log events whenever the L1 epoch ref
In the *Engine Queue* stage, the previously derived `PayloadAttributes` structures are buffered and sent to the In the *Engine Queue* stage, the previously derived `PayloadAttributes` structures are buffered and sent to the
[execution engine][g-exec-engine] to be executed and converted into a proper L2 block. [execution engine][g-exec-engine] to be executed and converted into a proper L2 block.
The engine queue maintains references to two L2 blocks: The stage maintains references to three L2 blocks:
- The [finalized L2 head][g-finalized-l2-head]: everything up to and including this block can be fully derived from the
[finalized][l1-finality] (i.e. canonical and forever irreversible) part of the L1 chain.
- The [safe L2 head][g-safe-l2-head]: everything up to and including this block can be fully derived from the - The [safe L2 head][g-safe-l2-head]: everything up to and including this block can be fully derived from the
canonical L1 chain. currently canonical L1 chain.
- The [unsafe L2 head][g-unsafe-l2-head]: blocks between the safe and unsafe heads are [unsafe - The [unsafe L2 head][g-unsafe-l2-head]: blocks between the safe and unsafe heads are [unsafe
blocks][g-unsafe-l2-block] that have not been derived from L1. These blocks either come from sequencing (in sequencer blocks][g-unsafe-l2-block] that have not been derived from L1. These blocks either come from sequencing (in sequencer
mode) or from [unsafe sync][g-unsafe-sync] to the sequencer (in validator mode). mode) or from [unsafe sync][g-unsafe-sync] to the sequencer (in validator mode).
This is also known as the "latest" head.
Additionally, it buffers a short history of references to recently processed safe L2 blocks, along with references
from which L1 blocks each was derived.
This history does not have to be complete, but enables later L1 finality signals to be translated into L2 finality.
#### Engine API usage
To interact with the engine, the [execution engine API][exec-engine] is used, with the following JSON-RPC methods:
If the unsafe head is ahead of the safe head, then [consolidation][g-consolidation] is attempted. [exec-engine]: exec-engine.md
- [`engine_forkchoiceUpdatedV1`] — updates the forkchoice (i.e. the chain head) to `headBlockHash` if different, and
instructs the engine to start building an execution payload if the payload attributes parameter is not `null`.
- [`engine_getPayloadV1`] — retrieves a previously requested execution payload build.
- [`engine_newPayloadV1`] — executes an execution payload to create a block.
[`engine_forkchoiceUpdatedV1`]: exec-engine.md#engine_forkchoiceupdatedv1
[`engine_getPayloadV1`]: exec-engine.md#engine_getpayloadv1
[`engine_newPayloadV1`]: exec-engine.md#engine_newpayloadv1
The execution payload is an object of type [`ExecutionPayloadV1`][eth-payload].
[eth-payload]: https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#executionpayloadv1
#### Forkchoice synchronization
If there are any forkchoice updates to be applied, before additional inputs are derived or processed, then these are
applied to the engine first.
This synchronization may happen when:
- A L1 finality signal finalizes one or more L2 blocks: updating the "finalized" L2 block.
- A successful consolidation of unsafe L2 blocks: updating the "safe" L2 block.
- The first thing after a derivation pipeline reset, to ensure a consistent execution engine forkchoice state.
The new forkchoice state is applied with `engine_forkchoiceUpdatedV1`.
On forkchoice-state validity errors the derivation pipeline must be reset to recover to consistent state.
#### L1-consolidation: payload attributes matching
If the unsafe head is ahead of the safe head, then [consolidation][g-consolidation] is attempted, verifying that
existing unsafe L2 chain matches the derived L2 inputs as derived from the canonical L1 data.
During consolidation, we consider the oldest unsafe L2 block, i.e. the unsafe L2 block directly after the safe head. If During consolidation, we consider the oldest unsafe L2 block, i.e. the unsafe L2 block directly after the safe head. If
the payload attributes match this oldest unsafe L2 block, then that block can be considered "safe" and becomes the new the payload attributes match this oldest unsafe L2 block, then that block can be considered "safe" and becomes the new
safe head. safe head.
In particular, the following fields of the payload attributes are checked for equality with the block: The following fields of the derived L2 payload attributes are checked for equality with the L2 block:
- `parent_hash` - `parent_hash`
- `timestamp` - `timestamp`
- `randao` - `randao`
- `fee_recipient` - `fee_recipient`
- `transactions_list` (first length, then equality of each of the encoded transactions) - `transactions_list` (first length, then equality of each of the encoded transactions, including deposits)
If consolidation succeeds, the forkchoice change will synchronize as described in the section above.
If consolidation fails, the unsafe L2 head is reset to the safe L2 head. If consolidation fails, the L2 payload attributes will be processed immediately as described in the section below.
The payload attributes are chosen in favor of the previous unsafe L2 block, creating an L2 chain reorg on top of the
current safe block. Immediately processing the new alternative attributes enables execution engines like go-ethereum to
enact the change, as linear rewinds of the tip of the chain may not be supported.
If the safe and unsafe L2 heads are identical (whether because of failed consolidation or not), we send the block to the #### L1-sync: payload attributes processing
execution engine to be converted into a proper L2 block, which will become both the new L2 safe and unsafe head.
If the safe and unsafe L2 heads are identical (whether because of failed consolidation or not), we send the L2 payload
attributes to the execution engine to be constructed into a proper L2 block.
This L2 block will then become both the new L2 safe and unsafe head.
If a payload attributes created from a batch cannot be inserted into the chain because of a validation error (i.e. there If a payload attributes created from a batch cannot be inserted into the chain because of a validation error (i.e. there
was an invalid transaction or state transition in the block) the batch should be dropped & the safe head should not be was an invalid transaction or state transition in the block) the batch should be dropped & the safe head should not be
...@@ -685,10 +723,136 @@ always valid. ...@@ -685,10 +723,136 @@ always valid.
Interaction with the execution engine via the execution engine API is detailed in the [Communication with the Execution Interaction with the execution engine via the execution engine API is detailed in the [Communication with the Execution
Engine][exec-engine-comm] section. Engine][exec-engine-comm] section.
The payload attributes are then processed with a sequence of:
- `engine_forkchoiceUpdatedV1` with current forkchoice state of the stage, and the attributes to start block building.
- Non-deterministic sources, like the tx-pool, must be disabled to reconstruct the expected block.
- `engine_getPayload` to retrieve the payload, by the payload-ID in the result of the previous step.
- `engine_forkchoiceUpdatedV1` to make the new payload canonical,
now with a change of both `safe` and `unsafe` fields to refer to the payload, and no payload attributes.
Engine API Error handling:
- On RPC-type errors the payload attributes processing should be re-attempted in a future step.
- On payload processing errors the attributes must be dropped, and the forkchoice state must be left unchanged.
- Eventually the derivation pipeline will produce alternative payload attributes, with or without batches.
- If the payload attributes only contained deposits, then it is a critical derivation error if these are invalid.
- On forkchoice-state validity errors the derivation pipeline must be reset to recover to consistent state.
#### Processing unsafe payload attributes
If no forkchoice updates or L1 data remain to be processed, and if the next possible L2 block is already available
through an unsafe source such as the sequencer publishing it via the p2p network, then it is optimistically processed as
an "unsafe" block. This reduces later derivation work to just consolidation with L1 in the happy case, and enables the
user to see the head of the L2 chain faster than the L1 may confirm the L2 batches.
To process unsafe payloads, the payload must:
- Have a block number higher than the current safe L2 head.
- The safe L2 head may only be reorged out due to L1 reorgs.
- Have a parent blockhash that matches the current unsafe L2 head.
- This prevents the execution engine individually syncing a larger gap in the unsafe L2 chain.
- This prevents unsafe L2 blocks from reorging other previously validated L2 blocks.
- This check may change in the future versions to adopt e.g. the L1 snap-sync protocol.
The payload is then processed with a sequence of:
- `engine_newPayloadV1`: process the payload. It does not become canonical yet.
- `engine_forkchoiceUpdatedV1`: make the payload the canonical unsafe L2 head, and keep the safe/finalized L2 heads.
Engine API Error handling:
- On RPC-type errors the payload processing should be re-attempted in a future step.
- On payload processing errors the payload must be dropped, and not be marked as canonical.
- On forkchoice-state validity errors the derivation pipeline must be reset to recover to consistent state.
### Resetting the Pipeline ### Resetting the Pipeline
It is possible to reset the pipeline, for instance if we detect an L1 [re-org][g-reorg]. For more details on this, see It is possible to reset the pipeline, for instance if we detect an L1 [reorg (reorganization)][g-reorg].
the [Handling L1 Re-Orgs][handling-reorgs] section. **This enables the rollup node to handle L1 chain reorg events.**
Resetting will recover the pipeline into a state that produces the same outputs as a full L2 derivation process,
but starting from an existing L2 chain that is traversed back just enough to reconcile with the current L1 chain.
Note that this algorithm covers several important use-cases:
- Initialize the pipeline without starting from 0, e.g. when the rollup node restarts with an existing engine instance.
- Recover the pipeline if it becomes inconsistent with the execution engine chain, e.g. when the engine syncs/changes.
- Recover the pipeline when the L1 chain reorganizes, e.g. a late L1 block is orphaned, or a larger attestation failure.
- Initialize the pipeline to derive a disputed L2 block with prior L1 and L2 history inside a fault-proof program.
Handling these cases also means a node can be configured to eagerly sync L1 data with 0 confirmations,
as it can undo the changes if the L1 later does recognize the data as canonical, enabling safe low-latency usage.
The Engine Queue is first reset, to determine the L1 and L2 starting points to continue derivation from.
After this, the other stages are reset independent of each other.
#### Finding the sync starting point
To find the starting point, there are several steps, relative to the head of the chain traversing back:
1. Find the current L2 forkchoice state
- If no `finalized` block can be found, start at the Bedrock genesis block.
- If no `safe` block can be found, fallback to the `finalized` block.
- The `unsafe` block should always be available and consistent with the above
(it may not be in rare engine-corruption recovery cases, this is being reviewed).
2. Find the first L2 block with plausible L1 reference to be the new `unsafe` starting point,
starting from previous `unsafe`, back to `finalized` and no further.
- Plausible iff: the L1 origin of the L2 block is known and canonical, or unknown and has a block-number ahead of L1.
3. Find the first L2 block with an L1 reference older than the sequencing window, to be the new `safe` starting point,
starting at the above plausible `unsafe` head, back to `finalized` and no further.
- If at any point the L1 origin is known but not canonical, the `unsafe` head is revised to parent of the current.
- The highest L2 block with known canonical L1 origin is remembered as `highest`.
- If at any point the L1 origin in the block is corrupt w.r.t. derivation rules, then error. Corruption includes:
- Inconsistent L1 origin block number or parent-hash with parent L1 origin
- Inconsistent L1 sequence number (always changes to `0` for a L1 origin change, or increments by `1` if not)
- If the L1 origin of the L2 block `n` is older than the L1 origin of `highest` by more than a sequence window,
and `n.sequence_number == 0`, then the parent L2 block of `n` will be the `safe` starting point.
4. The `finalized` L2 block persists as the `finalized` starting point.
5. Find the first L2 block with an L1 reference older than the channel-timeout
- The L1 origin referenced by this block which we call `l2base` will be the `base` for the L2 pipeline derivation:
By starting here, the stages can buffer any necessary data, while dropping incomplete derivation outputs until
L1 traversal has caught up with the actual L2 safe head.
While traversing back the L2 chain, an implementation may sanity-check that the starting point is never set too far
back compared to the existing forkchoice state, to avoid an intensive reorg because of misconfiguration.
Implementers note: step 1-4 are known as `FindL2Heads`. Step 5 is currently part of the Engine Queue reset.
This may change to isolate the starting-point search from the bare reset logic.
#### Resetting derivation stages
1. L1 Traversal: start at L1 `base` as first block to be pulled by next stage.
2. L1 Retrieval: empty previous data, and fetch the `base` L1 data, or defer the fetching work to a later pipeline step.
3. Frame Queue: empty the queue.
4. Channel Bank: empty the channel bank.
5. Channel Reader: reset any batch decoding state.
6. Batch Queue: empty the batch queue, use `base` as initial L1 point of reference.
7. Payload Attributes Derivation: empty any batch/attributes state.
8. Engine Queue:
- Initialize L2 forkchoice state with syncing start point state. (`finalized`/`safe`/`unsafe`)
- Initialize the L1 point of reference of the stage to `base`.
- Require a forkchoice update as first task
- Reset any finality data
Where necessary, stages starting at `base` can initialize their system-config from data encoded in the `l2base` block.
#### About reorgs Post-Merge
Note that post-[merge], the depth of reorgs will be bounded by the [L1 finality delay][l1-finality]
(2 L1 beacon epochs, or approximately 13 minutes, unless more than 1/3 of the network consistently disagrees).
New L1 blocks may be finalized every L1 beacon epoch (approximately 6.4 minutes), and depending on these
finality-signals and batch-inclusion, the derived L2 chain will become irreversible as well.
Note that this form of finalization only affects inputs, and nodes can then subjectively say the chain is irreversible,
by reproducing the chain from these irreversible inputs and the set protocol rules and parameters.
This is however completely unrelated to the outputs posted on L1, which require a form of proof like a fault-proof or
zk-proof to finalize. Optimistic-rollup outputs like withdrawals on L1 are only labeled "finalized" after passing a week
without dispute (fault proof challenge window), a name-collision with the proof-of-stake finalization.
[merge]: https://ethereum.org/en/upgrades/merge/
[l1-finality]: https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/#finality
------------------------------------------------------------------------------------------------------------------------ ------------------------------------------------------------------------------------------------------------------------
...@@ -696,9 +860,12 @@ the [Handling L1 Re-Orgs][handling-reorgs] section. ...@@ -696,9 +860,12 @@ the [Handling L1 Re-Orgs][handling-reorgs] section.
[deriving-payload-attr]: #deriving-payload-attributes [deriving-payload-attr]: #deriving-payload-attributes
For every L2 block we wish to create, we need to build [payload attributes][g-payload-attr], For every L2 block derived from L1 data, we need to build [payload attributes][g-payload-attr],
represented by an [expanded version][expanded-payload] of the [`PayloadAttributesV1`][eth-payload] object, represented by an [expanded version][expanded-payload] of the [`PayloadAttributesV1`][eth-payload] object,
which includes the additional `transactions` and `noTxPool` fields. which includes additional `transactions` and `noTxPool` fields.
This process happens during the payloads-attributes queue ran by a verifier node, as well as during block-production
ran by a sequencer node (the sequencer may enable the tx-pool usage if the transactions are batch-submitted).
[expanded-payload]: exec-engine.md#extended-payloadattributesv1 [expanded-payload]: exec-engine.md#extended-payloadattributesv1
[eth-payload]: https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#payloadattributesv1 [eth-payload]: https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#payloadattributesv1
...@@ -735,7 +902,8 @@ entries. ...@@ -735,7 +902,8 @@ entries.
[payload attributes]: #building-individual-payload-attributes [payload attributes]: #building-individual-payload-attributes
After deriving the transaction list, the rollup node constructs a [`PayloadAttributesV1`][expanded-payload] as follows: After deriving the transactions list, the rollup node constructs a [`PayloadAttributesV1`][extended-attributes] as
follows:
- `timestamp` is set to the batch's timestamp. - `timestamp` is set to the batch's timestamp.
- `random` is set to the `prev_randao` L1 block attribute. - `random` is set to the `prev_randao` L1 block attribute.
...@@ -745,281 +913,4 @@ After deriving the transaction list, the rollup node constructs a [`PayloadAttri ...@@ -745,281 +913,4 @@ After deriving the transaction list, the rollup node constructs a [`PayloadAttri
- `noTxPool` is set to `true`, to use the exact above `transactions` list when constructing the block. - `noTxPool` is set to `true`, to use the exact above `transactions` list when constructing the block.
- `gasLimit` is set to the current `gasLimit` value in the [system configuration][g-system-config] of this payload. - `gasLimit` is set to the current `gasLimit` value in the [system configuration][g-system-config] of this payload.
[expanded-payload]: exec-engine.md#extended-payloadattributesv1 [extended-attributes]: exec-engine.md#extended-payloadattributesv1
# Communication with the Execution Engine
[exec-engine-comm]: #communication-with-the-execution-engine
The [engine queue] is responsible for interacting with the execution engine, sending it
[`PayloadAttributesV1`][expanded-payload] objects and receiving L2 block references as a result. This happens whenever
the [safe L2 head][g-safe-l2-head] and the [unsafe L2 head][g-unsafe-l2-head] are identical, either because [unsafe
block consolidation][g-consolidation] failed or because no [unsafe L2 blocks][g-unsafe-l2-block] were known in the first
place. This section explains how this happens.
> **Note** This only describes interaction with the execution engine in the context of L2 chain derivation from L1. The
> sequencer also interacts with the engine when it needs to create new L2 blocks using L2 transactions submitted by
> users.
Let:
- `safeL2Head` be a variable in the state of the execution engine, tracking the (hash of) the current [safe L2
head][g-safe-l2-head]
- `unsafeL2Head` be a variable in the state of the execution engine, tracking the (hash of) the current [unsafe L2
head][g-unsafe-l2-head]
- `finalizedL2Head` be a variable in the state of the execution engine, tracking the (hash of) the current [finalized L2
head][g-finalized-l2-head]
- This is not yet implemented, and currently always holds the zero hash — this does not prevent the pseudocode below
from working.
- `payloadAttributes` be some previously derived [payload attributes][g-payload-attr] for the L2 block with number
`l2Number(safeL2Head) + 1`
[finality]: https://hackmd.io/@prysmaticlabs/finality
Then we can apply the following pseudocode logic to update the state of both the rollup driver and execution engine:
```javascript
fun makeL2Block(payloadAttributes) {
// request a new execution payload
forkChoiceState = {
headBlockHash: safeL2Head,
safeBlockHash: safeL2Head,
finalizedBlockHash: finalizedL2Head,
}
[status, payloadID, rpcErr] = engine_forkchoiceUpdatedV1(forkChoiceState, payloadAttributes)
if (rpcErr != null) return softError()
if (status != "VALID") return payloadError()
// retrieve and execute the execution payload
[executionPayload, rpcErr] = engine_getPayloadV1(payloadID)
if (rpcErr != null) return softError()
[status, rpcErr] = engine_newPayloadV1(executionPayload)
if (rpcErr != null) return softError()
if (status != "VALID") return payloadError()
newL2Head = executionPayload.blockHash
// update head to new refL2
forkChoiceState = {
headBlockHash: newL2Head,
safeBlockHash: newL2Head,
finalizedBlockHash: finalizedL2Head,
}
[status, payloadID, rpcErr] = engine_forkchoiceUpdatedV1(forkChoiceState, null)
if (rpcErr != null) return softError()
if (status != "SUCCESS") return payloadError()
return newL2Head
}
result = softError()
while (isSoftError(result)) {
result = makeL2Block(payloadAttributes)
if (isPayloadError(result)) {
payloadAttributes = onlyDeposits(payloadAttributes)
result = makeL2Block(payloadAttributes)
}
if (isPayloadError(result)) {
panic("this should never happen")
}
}
if (!isError(result)) {
safeL2Head = result
unsafeL2Head = result
}
```
> **TODO** `finalizedL2Head` is not being changed yet, but can be set to point to a L2 block fully derived from data up
> to a finalized L1 block.
As should apparent from the assignations, within the `forkChoiceState` object, the properties have the following
meaning:
- `headBlockHash`: block hash of the last block of the L2 chain, according to the sequencer.
- `safeBlockHash`: same as `headBlockHash`.
- `finalizedBlockHash`: the [finalized L2 head][g-finalized-l2-head].
Error handling:
- A value returned by `payloadError()` means the inputs were wrong.
- This could mean the sequencer included invalid transactions in the batch. **In this case, all transactions from the
batch should be dropped**. We assume this is the case, and modify the payload via `onlyDeposits` to only include
[deposited transactions][g-deposited], and retry.
- In the case of deposits, the [execution engine][g-exec-engine] will skip invalid transactions, so bad deposited
transactions should never cause a payload error.
- A value returned by `softError()` means that the interaction failed by chance, and should be reattempted (this is the
purpose of the `while` loop in the pseudo-code).
> **TODO** define "invalid transactions" properly, check the interpretation for the execution engine
The following JSON-RPC methods are part of the [execution engine API][exec-engine]:
[exec-engine]: exec-engine.md
- [`engine_forkchoiceUpdatedV1`] — updates the forkchoice (i.e. the chain head) to `headBlockHash` if different, and
instructs the engine to start building an execution payload if the payload attributes isn't `null`
- [`engine_getPayloadV1`] — retrieves a previously requested execution payload
- [`engine_newPayloadV1`] — executes an execution payload to create a block
[`engine_forkchoiceUpdatedV1`]: exec-engine.md#engine_forkchoiceUpdatedV1
[`engine_getPayloadV1`]: exec-engine.md#engine_newPayloadV1
[`engine_newPayloadV1`]: exec-engine.md#engine_newPayloadV1
The execution payload is an object of type [`ExecutionPayloadV1`][eth-payload].
[eth-payload]: https://github.com/ethereum/execution-apis/blob/main/src/engine/paris.md#executionpayloadv1
------------------------------------------------------------------------------------------------------------------------
# WARNING: BELOW THIS LINE, THE SPEC HAS NOT BEEN REVIEWED AND MAY CONTAIN MISTAKES
We still expect that the explanations here should be pretty useful.
------------------------------------------------------------------------------------------------------------------------
# Handling L1 Re-Orgs
[handling-reorgs]: #handling-l1-re-orgs
The [L2 chain derivation pipeline][pipeline] as described above assumes linear progression of the L1 chain.
If the L1 chain [re-orgs][g-reorg], the rollup node must re-derive sections of the L2 chain such that it derives the
same L2 chain that a rollup node would derive if it only followed the new L1 chain.
A re-org can be recovered without re-deriving the full L2 chain, by resetting each pipeline stage from end (Engine
Queue) to start (L1 Traversal).
The general idea is to backpropagate the new L1 head through the stages, and reset the state in each stage so that the
stage will next process data originating from that block onwards.
## Resetting the Engine Queue
The engine queue maintains references to two L2 blocks:
- The safe L2 block (or *safe head*): everything up to and including this block can be fully derived from the
canonical L1 chain.
- The unsafe L2 block (or *unsafe head*): blocks between the safe and unsafe heads are blocks that have not been
derived from L1. These blocks either come from sequencing (in sequencer mode) or from "unsafe sync" to the sequencer
(in validator mode).
When resetting the L1 head, we need to rollback the safe head such that the L1 origin of the new safe head is a
canonical L1 block (i.e. an the new L1 head, or one of its ancestors). We achieved this by walking back the L2 chain
(starting from the current safe head) until we find such an L2 block. While doing this, we must take care not to walk
past the [L2 genesis][g-l2-genesis] or L1 genesis.
The unsafe head does not necessarily need to be reset, as long as its L1 origin is *plausible*. The L1 origin of the
unsafe head is considered plausible as long as it is in the canonical L1 chain or is ahead (higher number) than the head
of the L1 chain. When we determine that this is no longer the case, we reset the unsafe head to be equal to the safe
head.
> **TODO** Don't we always need to discard the unsafe head when there is a L1 re-org, because the unsafe head's origin
> builds on L1 blocks that have been re-orged away?
>
> I'm guessing maybe we received some unsafe blocks that build upon the re-orged L2, which we accept without relating
> them back to the safe head?
## Resetting Payload Attribute Derivation
In payload attribute derivation, we need to ensure that the L1 head is reset to the safe L2 head's L1 origin. In the
worst case, this would be as far back as `SWS` ([sequencing window][g-sequencing-window] size) blocks before the engine
queue's L1 head.
In the worst case, a whole sequencing window of L1 blocks was required to derive the L2 safe head (meaning that
`safeL2Head.l1Origin == engineQueue.l1Head - SWS`). This means that to derive the next L2 block, we have to read data
derived from L1 block `engineQueue.l1Head - SWS` and onwards, hence the need to reset the L1 head back to that value for
this stage.
However, in general, it is only necessary to reset as far back as `safeL2Head.l1Origin`, since it marks the start of the
sequencing window for the safe L2 head's epoch. As such, the next L2 block never depends on data derived from L1 blocks
before `safeL2Head.l1Origin`.
> **TODO** in the implementation, we always rollback by SWS, which is unecessary
> Quote from original spec:"We must find the first L2 block whose complete sequencing window is unchanged in the reorg."
> **TODO** sanity check this section, it was incorrect in previous spec, and confused me multiple times
## Resetting Batch Decoding
The batch decoding stage is simply reset by resetting its L1 head to the payload attribute derivation stage's L1 head.
(The same reasoning as the payload derivation stage applies.)
## Resetting Channel Buffering
> **Note** in this section, the term *next (L2) block* will refer to the block that will become the next L2 safe head.
> **TODO** The above can be changed in the case where we always reset the unsafe head to the safe head upon L1 re-org.
> (See TODO above in "Resetting the Engine Queue")
Because we group [sequencer batches][g-sequencer-batch] into [channels][g-channel], it means that decoding a batch that
has data posted (in a [channel frame][g-channel-frame]) within the sequencing window of its epoch might require [channel
frames][g-channel-frame] posted before the start of the [sequencing window][g-sequencing-window]. Note that this is only
possible if we start sending channel frames before knowing all the batches that will go into the channel.
In the worst case, decoding the batch for the next L2 block would require reading the last frame from a channel, posted
in a [batcher transaction][g-batcher-transaction] in `safeL2Head.l1Origin + 1` (second L1 block of the next L2 block's
epoch sequencing window, assuming it is in the same epoch as `safeL2Head`).
> **Note** In reality, there are no checks or constraints preventing the batch from landing in `safeL2Head.l1Origin`.
> However this would be strange, because the next L2 block is built after the current L2 safe block, which requires
> reading the deposits L1 attributes and deposits from `safeL2Head.l1Origin`. Still, a wonky or misbehaving sequencer
> could post a batch for the L2 block `safeL2Head + 1` on L1 block `safeL2Head.1Origin`.
Keeping things worst case, `safeL2Head.l1Origin` would also be the last allowable block for the frame to land. The
allowed time range for frames within a channel to land on L1 is `[channel_id.number, channel_id.number +
CHANNEL_TIMEOUT]`. The allowed L1 block range for these frames are any L1 block whose number falls inside this block
range.
Therefore, to be safe, we can reset the L1 head of Channel Buffering to the L1 block whose number is
`safeL2Head.l1Origin.number - CHANNEL_TIMEOUT`.
> **Note** The above is what the implementation currently does.
In reality it's only strictly necessary to reset the oldest L1 block whose timestamp is higher than the oldest
`channel_id.timestamp` found in the batcher transaction that is not older than `safeL2Head.l1Origin.timestamp -
CHANNEL_TIMEOUT`.
We define `CHANNEL_TIMEOUT = 50`, i.e. 10mins
> **TODO** does `CHANNEL_TIMEOUT` have a relationship with `SWS`?
>
> I think yes, it has to be shorter than `SWS` but ONLY if we can't do streaming decryption (the case currently).
> Otherwise it could be shorter or longer.
— and explain its relationship with `SWS` if any?
This situation is the main purpose of the [channel timeout][g-channel-timeout]: without the timeout, we might have to
look arbitrarily far back on L1 to be able to decompress batches, which is not acceptable for performance reasons.
The other puprose of the channel timeout is to avoid having the rollup node keep old unclosed channel data around
forever.
Once the L1 head is reset, we then need to discard any frames read from blocks more recent than this updated L1 head.
## Resetting L1 Retrieval & L1 Traversal
These are simply reset by resetting their L1 head to `channelBuffering.l1Head`, and dropping any buffered data.
## Reorgs Post-Merge
Note that post-[merge], the depth of re-orgs will be bounded by the [L1 finality delay][l1-finality] (every 2 epochs, or
approximately 12 minutes, unless an attacker controls more than 1/3 of the total stake).
[merge]: https://ethereum.org/en/upgrades/merge/
[l1-finality]: https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/#finality
> **TODO** This was in the spec:
>
> In practice, we'll pick an already-finalized L1 block as L2
> inception point to preclude the possibility of a re-org past genesis, at the cost of a few empty blocks at the start
> of the L2 chain.
>
> This makes sense, but is in conflict with how the [L2 chain inception][g-l2-chain-inception] is currently determined,
> which is via the L2 output oracle deployment & upgrades.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment