This proposal introduces a new precompiled contract L1SLOAD
that loads several storage slots from L1 given a contract address and storage keys.
With the plethora of L2s on the Ethereum, building multi-chain smart contracts has become challenging. This proposal provides a convenient and trustless way for smart contracts deployed on an L2 chain to read storage values from L1. This improves the developer experience by removing the need for developers to generate and submit MPT proofs themselves.
An example use case is key management for smart accounts (multisigs and AA wallets). When a wallet already exists on L1, users no longer need to set up the configuration and signing keys on L2 but they can instead load them directly from L1. We believe there are many other use cases that could benefit from direct access to L1 state.
There have been similar proposals before from the community. Brecht Devos proposed L1CALL
that allows contracts on L2 to call contracts deployed on L1. Optimism had a similar RFP for remote static call. While the proposed static call mechanism is more powerful than simple state reads, it forces L1 EVM execution to be part of L2s which hinders its adoption by L2 chains. The L1SLOAD
provides more fundamental functionality and allows more flexibility to L2s because (a) it is easier for L2s to modify the EVM and (2) this precompile might be implemented even by totally non-EVM compatible L2s.
Name | Value |
---|---|
PRECOMPILED_ADDRESS | TBD |
FIXED_GAS_COST | 2000 (tentative) |
PER_LOAD_GAS_COST | 2000 |
MAX_NUM_STORAGE_SLOTS | 5 (tentative) |
The inputs to the L1SLOAD
precompile are an L1 contract address and storage keys up to MAX_NUM_STORAGE_SLOTS
.
Byte range | Name | Description |
---|---|---|
[0: 19] (20 bytes) | address |
The contract address |
[20: 51] (32 bytes) | key1 |
The storage key |
… | … | … |
[k *32-16: k *32+16] (32 bytes) |
keyk |
The storage key |
The output is the L1 storage value at the latest L1 block number known to the L2 sequencer.
Byte range | Name | Description |
---|---|---|
[0: 31] (32 bytes) | value1 |
The L1 storage value |
… | … | … |
[(k -1)*32: k *32] (32 bytes) |
valuek |
The L1 storage value |
Prerequisite 1: The L2 sequencer has access to an L1 node. Given that the sequencer needs to monitor deposit transactions from L1, it already embeds an L1 node inside (preferred) or has access to an L1 PRC endpoint.
The introduction of the L1SLOAD
precompile may increase the requirement for the L1 node or the L1 RPC endpoint to be an archive node when the L2 node syncs the L2 chains from older blocks.
Prerequisite 2: The L2 sequencer has a notion of the latest seen L1 block, which is deterministic over all L2 nodes, i.e. it is part of the L2 state machine. The exact mechanism is not in scope for this RIP.
Implementation: When the L2 node encounters a call to the L1SLOAD
precompiled contract, it first verifies that its input is well-formed. It then retrieves its latest seen L1 block number l1BlockNumber
and sends an RPC query eth_getStorageAt(address, storageKey, l1BlockNumber)
to the L1 node. Finally, it writes the received storage value to the designated output buffer.
There are a few error cases that the L1SLOAD
precompile needs to handle
The gas costs for L1SLOAD
is FIXED_GAS_COST + k * PER_LOAD_GAS_COST
, where k
is the number of storage slots. The constants are subject to change after more benchmarks.
The FIXED_GAS_COST
accounts for the additional RPC call latency to the L1 client. All storage keys are treated as cold keys in the L1SLOAD
and thus uses 2000 gas for each storage slots to be loaded.
Here is an example Solidity code snippet to use the L1SLOAD
precompile.
L1SLOAD
read the storage value at?According to the specification defined above, L1SLOAD
returns the storage value at the latest L1 block known to the L2 sequencer. There are two related issues:
L1SLOAD
First, to ensure the return value is consistent during transaction replay, L2 chains should provide a system contract that stores the information of the latest L1 block known to L2 sequencer. Optimism already provides a predeployed contract L1Block
. Scroll has a new system contract design that trustlessly imports the L1 block information and also stores other header fields such as state root, timestamp, RANDAO, etc.
Second, L2 protocols determine the L1 block import delay at their own discretion. To make L1SLOAD
more useful and reduce the risk of reading stale L1 storage states, we argue that the import delay should not be too long, e.g., waiting for finalized state that usually takes about 18-19 minutes. We suggest to wait for around 10 L1 block confirmations that has low risk of Ethereum chain re-organization while the import delay is fairly short (around 2 min). To adopt this, the L2 sequencers need to be capable of handling the situation when there is a long L1 chain re-organization. Furthermore, if the application is sensitive to stale storage reads, developers can limit the difference between the L1 block number retrieved from the system contract and the latest L1 block number per application requirement.
The L1SLOAD
precompile introduces risks of additional RPC latency and intermittent RPC errors. Both risks can be mitigated by running a L1 node in the same cluster as the L2 sequencer. It is preferrable for a L2 operator to run their own L1 node instead of using third party to get better security and reliability. We will perform more benchmarks to quantify the latency overhead in such setting.
Since the L1SLOAD
precompile directly returns storage values without verifying Merkle inclusion proofs, the responsibility of proving the correctness comes to L2 protocols. Here we briefly describe the method to prove the L1SLOAD
:
No backward compatibility issues found as the precompiled contract will be added to PRECOMPILED_ADDRESS
at the next available address in the precompiled address set.