- Linea architecture - alpha v3
- Transaction execution and management
- Gas price setting
- L1 <-> L2 interactions
- Finalized block tag on L2
The objective of the Linea network is to provide the functionality of Ethereum network at a fraction of the cost of Ethereum mainnet while providing guarantees on the correctness of its state.
Transaction execution is done using the Clique consensus protocol and correctness is guaranteed by providing proof of validity using zk-proofs.
The main components of Linea are:
- The sequencer which plays the role of the signer of the Clique protocol and creates blocks.
- The state manager for all L2 states which is used to store Linea state in a way that makes it easy to generate data used as inputs for zk-proof creation.
- The coordinator which orchestrates the different steps to create zk-proofs and persists them in the L1 Ethereum network. In particular, it's responsible for conflated batch creation, blob creation and aggregation
- The traces-APIs which provide trace counts and generate conflated trace files to be used for zk-proof creation
- The provers which generate zk-proof for conflated blocks, kzg commitment and compression proof of the blobs, as well as aggregated proof for multiple conflation and compression proofs.
The above diagram shows the flow from transaction to finalization. High level, the flow is as follow: (1) the coordinator pulls the latest block from the sequencer, (2) it gets the trace count from the traces API, (3) once a batch is ready it generates the conflated traces and gets the state transition, (4) request for an execution proof. (5) a prover generates the execution proof, (6) the coordinator packs a number of batches and generates a request for a blob, (7) a prover generates the compression proof. (8)The coordinator persists the blob on L1, (9) then requests an aggregation proof for the finalization which is provided by the prover (10) and then finally coordinator (11) posts the finalization proof on L1.
A file system is shared across multiple processes. It is used as a persistent queue.
The file system is used to store files for:
- Block traces
- Conflated-traces of batches
- Prover request (execution, compression & KZG commitment, aggregation)
- Proof responses (execution, compression & KZG commitment, aggregation)
A script automatically removes all files that are more than one week old.
There is a unique instance of Sequencer. It’s a special instance of a consensus client based on Besu. The consensus protocol used is Clique.
The sequencer receives the transactions pre-validated by the Ethereum nodes in order to execute them. It arranges and combines the transactions into blocks.
Blocks produced by the sequencer, in addition to common verifications, must fulfill the below requirements:
- trace counts are small enough to ensure the prover can prove the block
- the block total gas usage is below the gasLimit parameter
- And for each transaction:
a. gas fees are above minMinableGasPrice
b. They are ‘profitable’ i.e. the ratio between gas fees and compressed tx size is good enough.
c. Neither
from
norto
fields of the transaction correspond to a blacklisted user. d. calldata size is below the configured limit. e. gas limit is below the configured limit. - compressed block data can fit into a blob of size 128kB.
Transactions exceeding trace limits are added to an unexecutableTxList in-memory to avoid reconsidering them. Similarly transactions that take too long to be processed by the sequencer are added to the unexecutable list. Transactions from the unexecutable lists are removed from the pool.
Priority transactions are prioritized over normal ones. Priority transactions are those sent by a user whose address is in a predefined list. It typically corresponds to transactions triggered by the Linea system.
Note that if no transactions are received within the block window, no block is generated. This behavior differs from Ethereum mainnet, where empty blocks are still produced to maintain chain continuity and prevent certain attacks.
If a transaction could not be included in the current block, it will remain as a candidate for inclusion in the next block.
Endpoints exposed by the sequencer and used in the flow are:
eth_blockNumber():
blockNumber string
eth_getBlockByNumber(
blockNumber string
Transaction_detail_flag boolean
)
The state manager is configured differently based on the role it serves. The node described in this section is configured to serve the linea_getProof method, and has a different configuration than the one described in State manager - for latest L2 states.
The publicly exposed method linea_getProof is served by the state manager. We have a single instance of state manager.
The state manager keeps its representation of the Linea state by applying the block transactions computed by the sequencer, which it receives through P2P. It is composed of two parts:
- a Besu node with a Shomei plugin
- and a Shomei node.
The Besu node is connected to the other Ethereum nodes using P2P, and from the blocks it receives, it generates a trielog which it sends to the Shomei node. The trielog is a list of state modifications that occurred during the execution of the block. As soon as Shomei receives this trielog it will apply these changes to his state. The Shomei nodes use Sparse Merkle trees to represent Linea’s state.
Besu and Shomei nodes know each other's IP and port for direct communication.
The Shomei plugin implements the RPC endpoint to return rollup proofs. Rollup proofs are used to provide a Merkle proof of the existence of a key in a given block number for a specific address. This proof can be used onchain in the Ethereum L1 for the blocks that have been finalized given their root hashes are present onchain.
This state manager instance, which is configured to handle the linea_getProof requests, is configured to store only the states corresponding to the L2 blocks finalized on L1.
To serve the linea_getProof requests, the Shomei instance needs to know the latest L2 block finalized on L1. It is pushed by the coordinator to Shomei through a dedicated RPC endpoint. The IP address and port of Shomei are set in the coordinator configuration, allowing direct communication.
The endpoint linea_getProof
is as follows:
linea_getProof(
address string
key List[string]
blockNumber string
):
result: {
accountProof MerkleProof
storageProofs List[MerkleProof[^1]]
}
This component is identical to the one described in State manager - for L1 finalized states except that it is configured to compute and keep the state of each L2 block.
The IP address of Shomei is set in the coordinator configuration, allowing direct communication. The coordinator calls it to get proofs of state changes that are used as input for the prover.
End point exposed by the state manager and used in the flow are:
rollup_getZkEVMStateMerkleProofV0(
startBlockNumber int
endBlockNumber int
zkStateManagerVersion string
):
zkParentStateRootHash string
zkStateMerkleProof StateMerkleProof[^1]
zkStateManagerVersion string
The coordinator is responsible for
- deciding how many blocks can be conflated into a batch,
- deciding when to create a blob,
- deciding when an aggregation is generated,
- Posting proofs and data to L1.
For this, it gets the blocks from the sequencer using Json-RPC eth_blockNumber
andeth_getBlockByNumber
methods. This is done every ‘block time’ seconds.
The coordinator has a candidate list of blocks to create the next batch. For every new block, the coordinator checks if it can be added to the candidate list while respecting the batch requirements (data size and line count limits). If a block cannot be added, a new batch is created from the candidate list, and a new list is started containing the new block.
When the coordinator creates of a batch of blocks, it must ensure that:
- The data of all blocks of a batch can fit in a single blob or in the call data of a single transaction (size limit).
- Each batch can be proven in a conflation-proof (trace counts limit).
For each block, the coordinator gets the trace counts by calling a service implementing traces-api linea_getBlockTracesCountersV2
JSON-RPC method.
It checks the total compressed size of the transactions in the batch under construction using the compressor library canAppendBlock
method of the compressor.
In case of low activity, the coordinator triggers batch creation after some time out.
Upon batch creation, the coordinator gathers the inputs to create an execution proof request, namely: the conflated traces using linea_generateConflatedTracesToFileV2
, and a Merkle proof for the change of state between the first and the last block of the batch using rollup_getZkEVMStateMerkleProofV0
.
The coordinator collects all L2-L1 requests to be included in the batch looking at all logs emitted by l2MessageServiceAddress
for each block in the batch. The coordinator also collects L1->L2 anchoring message event logs.
The coordinator creates an execution proof request containing all information required to generate the proof.
Note: conflation step might be removed in future if aggregation turns out to be enough. However we keep it to optimize the resources of the provers in case of low activities.
The coordinator creates blobs by combining multiple contiguous batches. Upon creation of a blob, the coordinator must ensure that the total compressed size of the batches constituting it can fit in an EIP4844 blob (the size has to be at most 128kB, our current limit being 127 KB). To ensure profitability, the coordinator aims at fitting as much data in a blob as possible, hence it considers the size left in a blob while building batches. Dictated by constraints on the decompression prover, the plain size of batches on a blob must also add up to no more than around 800 KB.
If a batch cannot be added to the current, a blob is created, and a new blob construction is initiated with the last batch.
For every blob, the coordinator creates a KZG consistency proof and a compression proof request.
Once the proofs are generated, the coordinator sends the blob to L1 with the KZG proofs using blob storage.
Once a blob has been created, the coordinator retrieves a state change merkle proof from the state manager using rollup_getZkEVMStateMerkleProofV0.
It then calculates its schnarf using the go calculate schnarf library.
Finally it creates a proof request file for a KZG and a compression proof. This request file contains all required data to generate the proof. The coordinator is responsible to ensure that all data required by the prover is available before creating the request.
Once the KZG proof is generated, the coordinator sends a transaction to L1 LineaService with the blob and this proof as described in L1 finalization. In addition, it sends merkle tree roots of merkle trees storing L1L2 and L2L1 messages.
When sufficiently many batches and blobs have been generated, or after some maximum elapsed time, the coordinator creates a request file for the prover to generate an aggregation proof.
To build a request file, the coordinator needs information on the parent of the first block, its timestamp, the last L1-L2 message number and rolling hash anchored in this parent block. It gets these information using
L2MessageService.lastAnchoredL1MessageNumber and L2MessageService.l1RollingHashes.
The coordinator ensures that all data required by the prover is available before creating the request.
Once an aggregation proof is available and after some delay, the coordinator sends a finalization transaction to the L1 LineaService smart contract as described in L1 finalization. The smart contract verifies the caller entitlement, its solvability, the proof validity and then executes the L2 root hash state change.
If the coordinator is down no new batches, blob or aggregation will be produced, but blocks will still be produced by the sequencer. These new blocks will not be finalized on L1, similarly previously created batches, blob and aggregation proofs not yet submitted will not be submitted to L1 until the coordinator restarts.
This is a library written in go which takes as input a block and returns a byte array of the compressed data. Note that the signature of the sender of the transaction is also removed from the compressed data.
The compressor library is initialized with a data limit parameter and exposes the methods
startNewBatch
starts a new batch. Must be called between batches in the same blob
canWrite
returning true if the cumulative data once compressed has a size smaller than the data limit.
write
appends an RLP encoded block to the compressor
From the block, the compressor extract all transactions and format them keeping the following values:
For DynamicFeeTxType
Field | Type | DynamicFee | AccessList | Legacy |
---|---|---|---|---|
type | uint8 | ✅ | ✅ | ✅ |
nonce | uint64 | ✅ | ✅ | ✅ |
gasTipCap | int | ✅ | ||
gasFeeCap | int | ✅ | ||
gasPrice | int | ✅ | ✅ | |
gasLimit | int | ✅ | ✅ | ✅ |
from | byte[20] | ✅ | ✅ | ✅ |
to | byte[20] | ✅ | ✅ | ✅ |
value | int | ✅ | ✅ | ✅ |
data | bytes | ✅ | ✅ | ✅ |
accessList | AccessTuple[] | ✅ | ✅ | |
signature{r,s,v} | byte[] | |||
chainId | int |
AccessTuple
Address string
StorageKeys List[string]
Each block is an RLP encoded list, consisting of a timestamp and a list of transactions as described above.
Block
blocktime uint64 // block timestamp
encodedTransactions Transaction // list of transactions
In order to use the EIP4844 KZG commitment pre-compiles, we need to ensure that the blob as stored is a sequence of 32 byte, BLS12-381 scalar field elements. Since the scalar field modulus is a 255-bit number, we can fit 254 bits of arbitrary data in each 32 byte chunk of the blob. This necessary zero padding consumes 1 KB of the blob’s size. The packing mechanism reads more significant bits of every input byte first, and writes to the more significant bits of the field elements first. The input is padded with 0xFF000000…
until it is exactly 127 KB long. This consumes at least another byte and thus gives us exactly 130047 bytes to work with.
We usually refer to the following data structure as “the blob”, although in actuality it is stored after being packed as described above. The blob consists of compressed payload, following a header containing metadata.
Blob header:
Header
version uint16 // of the blob spec; currently 0xffff
dictChecksum [32]byte
nbBatches uint16 // number of batches in payload
batchNbBytes []uint24 // size of each batch in bytes
The payload is compressed according to the Linea compress spec. In short, the compressed stream consists of a 3-byte header denoting the compress
spec version used, and whether the following payload is actually compressed (accounting for the rare cases where the “compression” algorithm backfires and results in expansion.)
Header:
Header
version uint16 // compressor spec version
bypassed uint8 // boolean
The payload that follows (the main bulk of the blob) is a sequence of RLP encoded blocks. The boundary between batches is not marked here, as it is already specified in the blob header.
Go library to compute shnarf, commitment and kzg proofs. The library exposes a method calculateShnarf
as follow:
calculateShnarf(
eip4844Enabled boolean
compressedData string
parentStateRootHash string
finalStateRootHash string
prevShnarf string
conflationOrderStartingBlockNumber long
conflationOrderUpperBoundariesLen int
conflationOrderUpperBoundaries longArray
): CalculateShnarfResult
The returned data structure is:
CalculateShnarfResult
commitment string
kzgProofContract string
kzgProofSideCar string
dataHash string
snarkHash string
expectedX string
expectedY string
expectedShnarf string
errorMessage string
This component is used to generate line counts for each block and execution traces for batches. It is called by the coordinator. It is based on Geth and implements the traces-API endpoints. It has a representation of the ethereum state and executes the block to generate the traces.
Traces-API has two endpoints:
- Trace counts
linea_getBlockTracesCountersV2
returning the trace counts for a given block. - Conflated trace generation
linea_generateConflatedTracesToFileV2
used to trigger the generation of conflated traces. Conflated traces are expected to be of size 100MB~500MB (JSON + gzipped). They are stored in a shared folder inside shared/traces/conflated. This shared folder acts as a queue for other services. This call returns the name of the file once it is generated.
The number of trace generators is static for a given deployment. It’s determined in the deployment configuration.
The current implementation of traces-API proactively produces trace files for blocks produced as soon as it receives them via P2P. Each instance of traces-API is configured to handle blocks whose id is 0 modulo the instance number. This allows the generation of block traces across multiple instances.
Trace count works by loading the generated trace file of a block and returning the actual number of lines in the file.
Trace conflation consists in combining multiple trace files with some specific merging.
The endpoint exposed by the traces api and used in the flow are:
linea_getBlockTracesCountersV2(
blockNumber string
expectedTracesEngineVersion string
):
tracesEngineVersion string
tracesCounters map[string:string]
linea_generateConflatedTracesToFileV2(
startBlockNumber string
endBlockNumber string
expectedTracesEngineVersion string
):
tracesEngineVersion string
conflatedTracesFileName string //$first-$last.conflated.v$version.lt
linea_generateConflatedTracesToFileV2
is a blocking call. On completion, the trace API service persists the conflated trace to the shared file system and returns the conflated trace file name.
There are three types of proofs: execution, compression and aggregation. The proving flow is identical for all of them. Each prover can generate any of the three types of proofs.
The number of instances of prover is dynamic and driven by the number of proof requests in the file system. There is also one instance of a large prover running to handle requests that could not be handled by the regular prover instance.
In testnet, but not in the main Linea Layer, there are instances of provers that generate shorter proofs. The regular provers generate full proofs with standard memory settings. The large prover also generates full proof, the difference is that it is configured to have 1TB of memory. They are used to deal with proofs that unexpectedly cannot be handled by the default full prover (oom, too many constraints, …).
The proof sizes are estimated to be in the order of 2MB to 15MB.
The prover encapsulate two processes:
- One long running that monitors the file system and picks up proof requests.
- Another short running one which is instantiated when a request is picked up by the first process.
The long running process monitor triggers and monitors the short running process. It allows capturing unexpected issues occurring during the proving.
The short running process is further made up of two internal logic components: the traces expander (aka Corset) and the prover itself. Corset is responsible for expanding the execution traces into a format the prover can use before building the proof. The prover relies on the gnark library for zk-SNARKs implementation.
Corset is hosted inside the same process as the short running component of the prover. Traces are expanded by corset in memory and ingested by the prover directly, without the need for intermediate files to be sent over the network. This was motivated by the following reasons:
- Simplification of the overall architecture, with one less component to manage/maintain;
- Expanded traces is the largest file/chunk of data in the system, 100-500MB gzipped atm;
- No need to send large file over the network
- No need to decompress/deserialize expanded traces in the prover, which can be heavy when traces are large ~5GB;
- Reduce the probability of incompatibility between Corset/Prover versions and their input/output formats;
- Reduce latency of the overall system (rather a beneficial side effect than a driving motivation);
The paragraphs highlights the roles of the different proofs that are generated. Please refer to the prover backend codebase here for details on the objects and attributes for various types of proof requests and responses
It validates the correct execution of transactions within the Ethereum Virtual Machine (EVM). The proof system for the execution has a complex structure which involves the Vortex proof system, GKR and PLONK. The final proof takes the form of a BLS12-377-based PLONK proof.
An execution request proof is a file stored in the shared filesystem under the repository: with file name pattern: $startBlockNumber-$endBlockNumber-etv$tracesVersion-stv$stateManagerVersion-getZkProof.json
Requests folder name /shared/prover-execution/requests
Response folder name /shared/prover-execution/responses
Inprogress suffix: .inprogress
The request file contains this structure:
ProofRequest
zkParentStateRootHash bytes32
conflatedExecutionTracesFile string
tracesEngineVersion string
type2StateManagerVersion string
zkStateMerkleProof arrayNode
blocksData List[BlockData]
BlockData
rlp string // hex encoded
bridgeLogs List[RlpBridgeLogsData]
RlpBridgeLogsData
address string
topics List[string]
data string
blockNumber string
transactionHash string
transactionIndex string
blockHash string
logIndex string
removed boolean
The response file contains this file structure:
ProofResponse
proof string // hex encoded
proverMode string
verifierIndex uint // deprecated
verifyingKeyShaSum string // hex encoded
blocksData List[ResponseBlockData]
parentStateRootHash string // hex encoded
hasParentStateRootHashMismatch bool
proverVersion string
firstBlockNumber int
execDataChecksum bytes32
chainID uint
l2BridgeAddress [20]byte
maxNbL2MessageHashes int
allRollingHashEvent List[RollingHashUpdatedEvent]
allL2L1MessageHashes List[string] // hex encoded
publicInput bytes32
ResponseBlockData
blockHash bytes32
rlpEncodedTransactions List[string] // hex encoded
l2ToL1MsgHashes List[bytes32] // hex encoded
timestamp uint64
rootHash bytes32 // hex encoded
fromAddresses List[string] // hex encoded
batchReceptionIndices List[uint16]
lastRollingHashUpdatedEvent RollingHashUpdatedEvent
RollingHashUpdatedEvent
messageNumber int64
rollingHash string
Verifies the effective compression of a byte stream of data, which represents the inputs for the EVM execution circuit. This circuit ensures that the compressed data submitted on Ethereum can be accurately decompressed, revealing the necessary inputs for validation. The proof system used for generating the compression proof is PLONK and is based on the curve BLS12-377. \
File name
$startBlockNumber-$endBlockNumber-bcv$blobCalculatorVersion-ccv$conflationCalculatorVersion-getZkBlobCompressionProof.json
Requests folder name /shared/prover-compression/requests
Response folder name /shared/prover-compression/responses
Inprogress suffix: .inprogress
Compression proof request file format:
BlobCompressionProofJsonRequest
eip4844Enabled boolean
compressedData string // base64 encoded
dataParentHash string
conflationOrder ConflationOrder
parentStateRootHash string // hex encoded
finalStateRootHash string // hex encoded
prevShnarf string
ConflationOrder
startingBlockNumber int
upperBoundaries List[int]
Compression proof response file format:
BlobCompressionProofJsonResponse
eip4844Enabled boolean
dataHash string
compressedData string // kzg4844.Blob [131072]byte. The data that are explicitly sent in the blob (i.e. after compression)
commitment string // kzg4844.Commitment [48]byte
kzgProofContract string
kzgProofSidecar string
expectedX string
expectedY string
snarkHash string
conflationOrder ConflationOrder
parentStateRootHash string
finalStateRootHash string
parentDataHash string
expectedShnarf string
prevShnarf string
Serves as the cornerstone of Linea's proof system, recursively verifying proofs from N execution circuits and M compression circuit instances. This circuit encapsulates the primary statement of Linea's prover and is the sole circuit subjected to external verification. The proof system used is a combination of several PLONK circuits on BW6, BLS12-377 and BN254 which tactically profits from the 2-chained curves BLS12-377 and BW6 to efficiently recurse the proofs. The final proof takes the form of a BN254 curve that can be efficiently verified on Ethereum thanks to the available precompiles.
File name
$startBlockNumber-$endBlockNumber-getZkAggregatedProof.json
Requests folder name /shared/prover-aggregation/requests
Response folder name /shared/prover-aggregation/responses
Inprogress suffix: .inprogress
AggregationProofJsonRequest
executionProofs List[String]
compressionProofs List[String]
parentAggregationLastBlockTimestamp uint64
parentAggregationLastL1RollingHash string
parentAggregationLastL1RollingHashMessageNumber int
ProofToFinalizeJsonResponse
finalShnarf string
parentAggregationFinalShnarf string
aggregatedProof string // hex encoded
aggregatedProverVersion string // hex encoded
aggregatedVerifierIndex int
aggregatedProofPublicInput string
dataHashes List[string]
dataParentHash string
parentStateRootHash string
parentAggregationLastBlockTimestamp uint
lastFinalizedBlockNumber uint
finalTimestamp uint
finalBlockNumber uint
l1RollingHash string
l1RollingHashMessageNumber uint
l2MerkleRoots List[string]
l2MerkleTreesDepth uint
l2MessagingBlocksOffsets string
The web3 signer is a generic system used by the coordinator to sign the transaction it sends to L1.
This allows authenticating the transaction in order to pay for the gas fees and to handle entitlement of the smart contract being called.
The web3 signer exposes the following REST endpoint which is used in the flow:
/api/v1/eth1/sign/${publicKey}(
data bytes
):
signature string
It is deployed as a HA system with primary and secondary pods. Transactions are signed by both primary and secondary. The keys are isolated from the core blockchain environment. Web3Signer is accessed by the Coordinator using a AWS VPC Private link feature.
This is the L2 smart contract message service used to anchor messages submitted to L1.
It is responsible to ensure that messages are claimed at most once.
Methods
lastAnchoredL1MessageNumber():
lastAnchoredL1MessageNumber uint256
l1RollingHashes(
messageNumber uint256
):
rollingHash bytes32
Gas pricing on Linea is designed to ensure the following three properties:
- Sequencer's inclusion logic is aligned to the L1 fee market. This is to avoid exploiting Linea to execute transactions for unsustainably low fees
- The fees charged to Linea's user represent their fair usage of the network. Unlike the vanilla Ethereum protocol, gas price on Linea and other rollups is not 2-dimensional (base fee, priority fee). There's at least L1 fees (execution fees and blob fees), infrastructural cost (mostly proving, but not only), potential priority fee (only when there's a high congestion and there's competition for L2 block space). This is an issue for interoperability, because vanilla Ethreum API isn't tailored for this. That's why there's a Besu plugin addressing this issue and providing gas price depending on input transaction
- Linea remains compatible with users running vanilla nodes. Namely,
eth_gasPrice
returns fees guaranteeing that 99.9% of transactions are includable on Linea.
This is how these challenges were solved technically:
The Coordinator fetches L1 fees data, based on which it will compute gas pricing components. There are 3 of them:
- Fixed cost. Represents infrastructural cost per unit of L2 gas. Doesn't really depend on the L1, and it's just a configuration in the Coordinator
- Variable cost. Cost of 1 byte of compressed data on L2, which is finalized on L1 contract. Depends on the fees Linea pays for finalization, which in turn depends on the L1 blob and execution fee market
- Legacy cost. Recommended gas price for the vanilla Ethereum API (
eth_gasPrice
)
This information is delivered to nodes in 2 ways:
- it is added to the extraData block field, part of the vanilla Ethereum Protocol
- via RPC calls (only Geth and Besu are supported and tested)
The Coordinator sends extraData to the Sequencer via miner_setExtraData
. ExtraData contains all 3 fields mentioned above (fixed cost, variable cost and legacy cost).
The Sequencer in turn uses this information for inclusion logic, to include only profitable transactions, and it adds last
received extraData to the next block it seals. This ensures that pricing information is propagated to all the nodes on Linea
via P2P as a block header's field. And since this info is on all the nodes, they can use this information to figure out,
what the gas price is for a given transaction that would make it includable on Linea. This currently is possible with Besu +
Linea plugin with a custom linea_estimateGas
method.
For nodes that are reachable from the Coordinator directly, it's possible to set legacy cost via miner_setGasPrice
(Geth)
and miner_setMinGasPrice
(Besu). Later isn't really used, because extraData driven approach is superior and is
supported by Besu nodes with Linea plugin
In the Coordinator 2 ways are supported:
- So called "naive" way. Based on raw L1 fees processed by some formula
- So called "sample transaction" way. The idea is to take some relatively unprofitable transaction, estimate its profitable gas price the same way Sequencer would. Resulting value would be used as a legacy cost. this is configured by 2 arguments to a profitability function: execution gas and tx compressed size and it may be changed depending on what load is there on Linea.
There are three types of integration:
- Block finalization, specific to Linea, it’s used to persist on L1 the state changes happening on L2. This happens in two steps, first blob data and KZG proofs are persisted on L1, then aggregation proofs are sent to L1
- L1 -> L2, typically used to transfer funds from L1 to L2.
- L2 -> L1, to retrieve funds from L2 back to L1.
A summary of each of them is given below.
The coordinator submits up to six blobs it generates at once to L1 using eip4844 standard to v3.1 LineaRollup.submitBlobs
alongside
the KZG proof. The LineaRollup smart contract verifies the validity of the proofs for the given blob data.
Blob submission can support sending up to six blobs at once. This allows saving cost by amortizing the processing overhead over multiple blobs.
submitBlobs(
blobSubmissionData BlobSubmissionData[]
parentShnarf bytes32
finalBlobShnarf bytes32
)
Where:
BlobSubmissionData
submissionData SupportingSubmissionDataV2
dataEvaluationClaim uint256
kzgCommitment bytes
kzgProof bytes
and
SupportingSubmissionDataV2
finalStateRootHash bytes32
firstBlockInData uint256
finalBlockInData uint256
snarkHash bytes32
L1 finalization is triggered by the coordinator once an aggregation proof has been generated. This is done by triggering
a transaction to execute LineaService.finalizeCompressedBlocksWithProof
method on L1. In the process, the aggregation
proof is sent to L1. Once the transaction is completed on L1, all the blocks are finalized on L2.
The interface use is described bellow labeled with their respective Linea release version.
finalizeBlocksWithProof(
aggregatedProof bytes
proofType uint256
finalizationData FinalizationDataV2
)
where
FinalizationDataV2
parentStateRootHash bytes32
lastFinalizedShnarf bytes32
finalBlockInData uint256
shnarfData ShnarfData
lastFinalizedTimestamp uint256
finalTimestamp uint256
lastFinalizedL1RollingHash bytes32
l1RollingHash bytes32
lastFinalizedL1RollingHashMessageNumber uint256
l1RollingHashMessageNumber uint256
l2MerkleTreesDepth uint256
l2MerkleRoots bytes32[]
l2MessagingBlocksOffsets bytes
Note that a method which can be used by the security council is also available to finalize a block without proof:
finalizeCompressedBlocksWithoutProof(
finalizationData FinalizationData
)
Cross-chain operations happen when a user triggers a transaction on L1 by calling the L1MessageService.sendMessage
method of message service smart contract, where L1MessageService is inherited by the deployed LineaRollup smart contract.
sendMessage(
to String
fee uint256
calldata bytes
)
Optional ETH value to be transferred can be set when calling the above method.
When such a transaction is executed on L1, it triggers the publication of a message in the chain log of events, and the user pays for gas related to the L1 transaction execution.
Internally, the message service computes a rolling hash. It’s computed recursively as the hash of the previous rolling hash and the new message hash.
Linea’s coordinator, which is subscribing to L1 events, detects the L1 finalized (2 epochs to avoid reorgs) cross-chain MessageSent event and anchors it on L2. The coordinator anchors the messages by batches.
Anchoring (Anchoring is the process for placing a "cross-chain validity reference", that must exist for any message to be claimed) of messages is done through the executed via anchorL1L2MessageHashes which is inherited by the L2MessageService smart contract.
To anchor a message, the coordinator collects all L1-L2 message logs from which it gets the message hash, the rolling hash, the nonce (unique message number) and the L1 block number. Note that the L1 block number facilitates the processing but is not anchored on L2.
Filtering takes place beforehand to exclude anchored messages by looking on L2 at the L2MessageService.inboxL1L2MessageStatus
for each hash to see if it’s already anchored
For those not yet anchored, it anchors the message hashes, recomputing the rolling hash and final message number using the L2MessageService
function. \
anchorL1L2MessageHashes(
messageHashes bytes32[]
startingMessageNumber uint256
finalMessageNumber uint256
finalRollingHash bytes32
)
The rolling hash passed as parameter to this method serves to perform a soft validation that the messages anchored on L2 lead to the expected rolling hash computed on L1. It is used to detect inconsistency early, but it does not provide provable guarantees. The full benefit of the rolling hash is only really realized on L1 finalization as part of the feedback loop.
Checking that the rolling hash and message number matches, ensures that the anchoring is censorship and tamper resistant.
Once the message is anchored on L2, it can then be claimed. Claims can be triggered via a Postman or manually.
The coordinator is the only operator entitled to anchor messages on L2.
Triggering via the Postman service will happen if the initial user asked for it and has prepaid the estimated fees. In this case, if the prepaid fees match the Postman service expectations (profitability based on gas costs etc.), it executes a claim transaction to call the L2 Service Message claimMessage method and the Postman service receives the prepaid estimated fees on L2.
Additionally, should the cost to execute the delivery be less than the fee paid, and the difference is cost effective to send to the message recipient, the difference is transferred to the destination user.
Claims can also be done manually. In this case, a user will trigger the claim transaction using the L2 Service Message and pay for the required gas fees.
claimMessage(
from address
to address
fee uint256
value uint256
feeRecipient address
calldata bytes
nonce uint256
)
When a message is claimed, an event is emitted using:
emit MessageClaimed(messageHash)
On finalization the value of the final (last in rollup data being finalized) RollingHashUpdated event on the L2 Service Message is validated against the expected value on L1 exists (using l1RollingHash bytes32
and l1RollingHashMessageNumber uint256
). A successful finalization guarantees that no message can be claimed on L2 that did not exist on L1, and that no messages were excluded on anchoring message hashes.
Whenever a transaction is executed on L2 to send a message to L1, a MessageSent event is emitted.
Txs to send L2 -> L1 messages are sent to the L2 Message Service function:
sendMessage(
to String
fee uint256
calldata bytes
)
Optional eth can be set, this value is then mapped to the value field of the MessageSent event.
Fee must have a minimum value of 0.0001 ETH to prevent DDOS attacks.
MessageSent(
from String
to String
fee uint256
value uint256
nonce uint256
calldata bytes
messageHash bytes32
)
A configurable fixed Merkle tree depth is set on the Prover.
The Prover collects ordered message hashes from all emitted MessageSent events contained in the blocks being finalized.
The Prover groups the message hashes based on the Merkle tree depth configured. E.g. a depth of 5 will group the hashes into groups of 2^5 (32) hashes.
A group will be filled with empty hashes to make 32 if it is partial.
The prover computes the Merkle root of each group.
During the finalization, the Merkle roots are anchored on L1.
The depth of the tree forms part of the finalization calldata.
An event is emitted per Merkle root anchoring along with the depth.
Offsets indicating which blocks post the last finalized block containing MessageSent events are sent during finalization.
The finalization process emits on L1 an event per L2 block containing an L2 MessageSent event.
Any party (e.g. via an npm package we provide for the bridge/partners etc) does the following :
- Takes a message on L2 noting the block the event was fired on.
- Finds the event (and transaction hash) where that block was emitted/finalized on L1 using the block number as a filter.
- Retrieves all block numbers from the events in that transaction.
- Retrieves the tree depth from the transaction events.
- Queries the L2 blocks in that list (or range - lowest-highest) to get all MessageSent events.
- Groups all the message hashes from the query into groups based on the tree depth from the Merkle root anchoring events. E.g. depth 5 results in 2^5 (32) hashes. A group will be filled with empty hashes to make 32 if it is partial.
- Picks the group for the required message hash and constructs a Merkle proof to claim against using the group's message hashes.
For a Linea besu node to support finalized
tag on Ethereum RPC methods (e.g. eth_getBlockByNumber
), the linea besu node would need to run with the finalized-tag-updater
plugin (download page and please note that it requires a Github access token for now but would soon to be publicly downloadable from linea-monorepo)
The plugin periodically calls the L1 Linea rollup contract's currentL2BlockNumber
method to retrieve the latest proven L2 block number from the current L1 finalized
block, and set the L2 block number as the finalized
(and safe
block number) to the plugin-hosting besu client.
For more information on how to run besu node with plugin, please check out the Besu official website