Rollup Data Structures
Table of Contents
Rollup State
We divide the rollup state into two components:
- An ordinary read-write state, committed to via the state root, which is a
bytes32 stateRoot
representing the Merkle root of the underlying indexed Merkle tree. - A write-only withdrawals state, committed to via the withdrawals root, which is a
bytes32 withdrawalsRoot
representing the Merkle root of the underlying indexed Merkle tree.
These two Merkle roots will be separately updated on L1 upon L2 block finalization, and the rollup logic must guarantee that withdrawals are only ever appended to.
Initially, the state and withdrawals indexed merkle tree are empty with their height set to zero.
Transaction Serialization and Validity
We require that transactions are serialized in two different ways:
- As raw bytes put in calldata or blobs for data availability.
- In a Solidity struct for L1-initiated transactions.
Each transaction must also be associated with a transaction hash, which is required to commit to each of its fields.
We say that a serialized transaction is valid relative to a rollup state if it passes two types of validity checks:
- State-independent validity: These checks enforce that the transaction is validly serialized and has the correct authentication. These checks should depend only on the transaction data and not on the rollup state. They are parallel to Geth's transaction validation pipeline.
- State-dependent validity: These are checks on the transaction which depend on the rollup state prior to the transaction. They typically include nonce checks or authentication against key data in the rollup state.
These two pieces of the validity check will be run in different parts of the protocol.
Preblock Format
Transactions are sequenced and committed to L1 in preblocks, which contain transactions and the following sequencer-determined metadata fields:
uint256 timestamp
- The sequencer-assigned timestamp of the preblock.
The sequencer will post preblocks in the following format. In particular, preblocks do not reference previous states.
struct L2Preblock {
uint256 timestamp;
Transaction[] transactions;
}
There may be at most MAX_TRANSACTIONS_PER_BLOCK
transactions in a preblock.
Batch Format
Preblocks enter the chain derivation pipeline in batches, which are arrays of preblocks combined with metadata fields. Batches can be created in two ways:
- Sequencer batches, which are created by the sequencer and posted to L1 via the bridge.
- L1-initiated transactions, which are created by users on L1 and included into batches after a delay of
L1_INITIATION_DELAY
L1 blocks.
The two types of batches are indexed by the sequentially increasing indices uint256 sequencerBatchIndex
and uint256 l1BatchIndex
, respectively, which are tracked in the rollup bridge. Each batch B
is associated with a sequencer batch and an L1-initiated batch as follows:
uint256 sequencerBatchIndex
-- The sequencer batch index of the most recent sequencer batch up toB
, possibly includingB
itself. If no such sequencer batch exists, this is0
.uint256 l1BatchIndex
-- The L1-initiated batch index of the most recent L1-initiated batch up toB
, possibly includingB
itself. If no such L1-initiated batch exists, this is0
.
The global position of a batch in the set of all batches is given by uint256 batchIndex
and is determined by batchIndex = sequencerBatchIndex + l1BatchIndex
. The specifics of the batch format are detailed in Sequencing and L1-Initiated Transactions.
Block Format
A rollup block consists of an ordered list of rollup transactions together with the following block metadata fields:
uint256 blockNumber
- The rollup block number, constrained to be sequentially increasing.uint256 timestamp
- The rollup timestamp, constrained to be non-decreasing and betweenblock.timestamp - MAX_SEQUENCER_DELAY
andblock.timestamp + MAX_SEQUENCER_DRIFT
by the state transition function.bytes32 parentHash
- The hash of the previous block.bytes32 sequencerKeystoreAddress
- The keystore address of the sequencer.bytes32 stateRoot
- The state root after execution of the block.bytes32 withdrawalsRoot
- The withdrawals root after execution of the block.bytes32 transactionsRoot
- The Merkle root of all transactions in the block, formatted to be the indexed Merkle tree root (with the Keccak hash) of the mapping with key-value pairs(idx, txHash[idx])
foridx = 0, ..., #transactions - 1
. If the block contains no transactions, this is the root of an empty indexed Merkle tree (with height zero).
It is represented by the following Solidity struct.
struct L2Block {
uint256 blockNumber;
uint256 timestamp;
bytes32 parentHash;
bytes32 sequencerKeystoreAddress;
bytes32 stateRoot;
bytes32 withdrawalsRoot;
bytes32 transactionsRoot;
Transaction[] transactions;
}
Each block has a blockhash, defined by
bytes32 blockhash = keccak256(abi.encodePacked(blockNumber, timestamp, parentHash, sequencerKeystoreAddress, state
Root, withdrawalsRoot, transactionsRoot));
At genesis, we adopt special values for the state root, withdrawals root, blockhash, block number, and timestamp, given by:
bytes32 GENESIS_STATE = keccak256("AxiomGenesisStateRoot");
bytes32 GENESIS_WITHDRAWALS = keccak256("AxiomGenesisWithdrawalsRoot");
bytes32 GENESIS_BLOCKHASH = keccak256("AxiomGenesisBlockhash");
uint256 GENESIS_BLOCK_NUMBER = 0;
uint256 GENESIS_BLOCK_TIMESTAMP = 0;
We also use the following special values for the first l1 and sequencer batch commitments:
bytes32 INIT_L1_BATCH_COMMITMENT = keccak256("AxiomInitL1BatchCommitment");
bytes32 INIT_SEQUENCER_BATCH_COMMITMENT = keccak256("AxiomInitSequencerBatchCommitment");