RIP-7859: Store L1 origin data and historic L2 block hashes in L2 state
Standardize the commitment of L1 origin data and historic L2 block hashes in L2 state
Abstract
In every L2 block store the block hash for the L1 block that the L2 block derives inputs from in a ring buffer inside
a pre-deployed smart contract. This ring buffer will hold the last L1_ORIGIN_BUFFER_LENGTH
number of origin hashes and
will be referred to as the L1 origin storage contract.
Additionally, in every L2 block store the last L2_HISTORY_BUFFER_LENGTH
historical L2 block hashes in a ring buffer
inside a pre-deployed smart contract. This contract will be referred to as the L2 history storage contract.
Motivation
This RIP is the combination of EIP-2935 unmodified and EIP-4788 with minor modification to store a hash of a subset of an L2 block's L1 origin execution block header fields instead of beacon block roots.
In combination these two improvements enable L2s that settle and derive from Ethereum to verify arbitrary claims about Ethereum and all the other L2s that settle to Ethereum.
L1 origin storage contract
Maintaining inside the L2 execution environment a view of the L1 state that a given L2 block derives from- that L2 block's L1 origin- is broadly useful for L1<->L2 and L2<->L2 interoperability and composability. Some specific examples of where this is useful include: RIP-7755, RIP-7789, RIP-7728, ERC-3668, and verifying AVS validator set state on the L1 for purposes of EigenDA or Lagrange state committees or other AVS protocols. In general, it is useful to any contract on the L2 that needs to verify arbitrary L1 state, storage, transaction, or receipt data inside the L2.
L2 history storage contract
Maintaining inside the L2 execution environment historic L2 block hashes is useful for the stated side-benefit provided as motivation for EIP-2935:
"A side benefit of this approach could be that it allows building/validating proofs related to last
L2_HISTORY_BUFFER_LENGTH
ancestors directly against the current state."
The value provided by this capability is increased in the context of L2s as most L2s only submit L2 outputs to the L1 at
sparse intervals. If the L2_HISTORY_BUFFER_LENGTH
is long enough to span the L2 outputs on the L1 then it is made
possible for any contract on the L1 to verify arbitrary L2 state, storage, transaction, or receipt data at any L2
height using the historical L2 block hashes stored in the L2 history storage contract of an L2 output.
Specification
Specification mirrors the specifications of EIP-2935 and EIP-4788 with modification of EIP-4788 such that the ring buffer stores a keccak256 hash of a subset of L1 execution block header fields instead of beacon block roots.
Some L2s already support EIP-4788 so to avoid a conflict we must select a different address for the pre-deployed
contract L1_ORIGIN_STORAGE_ADDRESS
.
If an L2 already supports EIP-2935 no modification to that contract is needed to support the L2 history storage contract. To fulfill this RIP's specification they only need to add support for the L1 origin storage contract.
L1 origin storage contract
Name | Value |
---|---|
L1_ORIGIN_BUFFER_LENGTH | 8192 |
L1_ORIGIN_SYSTEM_ADDRESS | 0xfffffffffffffffffffffffffffffffffffffffe |
L1_ORIGIN_STORAGE_ADDRESS | tbd |
L1_ORIGIN_FORK_TIMESTAMP | variable - defined by the L2 |
The L1 origin storage contract has two operations: get
and set
. The input itself is not used to determine which
function to execute, for that the result of caller
is used. If caller
is equal to L1_ORIGIN_STORAGE_ADDRESS
then
the operation to perform is set
. Otherwise, get
.
get
- Callers provide the
timestamp
they are querying encoded as 32 bytes in big-endian format. - If the input is not exactly 32 bytes, the contract must revert.
- If the input is equal to 0, the contract must revert.
- Given
timestamp
, the contract computes the storage index in which the timestamp is stored by computing the modulotimestamp % L1_ORIGIN_BUFFER_LENGTH
and reads the value. - If the
timestamp
does not match, the contract must revert. - Finally, the
keccak256([l1Origin.block.height, l1Origin.block.stateRoot, l1Origin.block.transactionRoot, l1Origin.block.receiptRoot])
associated with the timestamp is returned to the user. It is stored attimestamp % L1_ORIGIN_BUFFER_LENGTH + L1_ORIGIN_BUFFER_LENGTH
.
set
- Caller (the sequencer) provides
keccak256([l1Origin.block.height, l1Origin.block.stateRoot, l1Origin.block.transactionRoot, l1Origin.block.receiptRoot])
as calldata to the contract. - Set the storage value at
header.timestamp % L1_ORIGIN_BUFFER_LENGTH
to beheader.timestamp
- Set the storage value at
header.timestamp % L1_ORIGIN_BUFFER_LENGTH + L1_ORIGIN_BUFFER_LENGTH
to becalldata[0:32]
Bytecode
The exact contract bytecode is shared below.
Deployment
The L1 origin storage contract can be deployed as a pre-deployed contract.
Block processing
At the top of every L2 block where block.timestamp >= L1_ORIGIN_FORK_TIMESTAMP
the sequencer will call
L1_ORIGIN_STORAGE_ADDRESS
as L1_ORIGIN_SYSTEM_ADDRESS
with the 32-byte input of
keccak256([l1Origin.block.height, l1Origin.block.stateRoot, l1Origin.block.transactionRoot, l1Origin.block.receiptRoot])
,
a gas limit of 30_000_000
, and 0
value. This will trigger the set()
routine of the beacon roots contract.
This is a system operation and therefore:
- the call must execute to completion
- the call does not count against the block's gas limit
- the call does not follow the EIP-1559 burn semantics - no value should be transferred as part of the call
- if no code exists at
L1_ORIGIN_STORAGE_ADDRESS
, the call must fail silently
If this EIP is active in a genesis block, no system transaction may occur.
l1origin
is defined as the L1 block from which inputs were taken in order to derive the nascent L2 block. If an L2
block is built without L1 inputs but the L2 maintains a strict mapping of L2->L1 blocks based on timestamps then the L1
origin is the L1 block that satisfies this mapping. If an L2 block is built without L1 inputs and the L2 maintains no
strict mapping of the L2->L1 blocks then the L1 origin remains the same as the L1 origin of the previous L2 block.
The invariants are
- Every L2 block has an L1 origin
- For an L2 block N with timestamp/height greater than L2 block M the L1 origin of N must have a height/timestamp greater than the L1 origin of M or share the same L1 origin
- If an L2 block includes transactions derived from L1 inputs the L1 origin for that L2 block must be the latest (highest height/timestamp) L1 block that contributed L1 inputs
The correctness of an L2 block according to the above invariants must be enforced at settlement time. This can be accomplished by codifying these constraints into an optimistic rollup's fault proof program or into a zk-rollup's validity circuit.
Additional constraints on the L1 origin may be enforced, such as a maximum difference between an L2 block's timestamp and the timestamp of its L1 origin block, but these are not required.
L2 history storage contract
Name | Value |
---|---|
L2_HISTORY_BUFFER_LENGTH | 8192 |
L2_HISTORY_SYSTEM_ADDRESS | 0xfffffffffffffffffffffffffffffffffffffffe |
L2_HISTORY_STORAGE_ADDRESS | tbd |
L2_HISTORY_FORK_TIMESTAMP | variable - defined by the L2 |
Note: an L2 could perform this upgrade incrementally such that L1_ORIGIN_FORK_TIMESTAMP
!= L2_HISTORY_FORK_TIMESTAMP
The history contract has two operations: get
and set
. The set
operation is invoked only when the caller
is equal
to the L2_HISTORY_SYSTEM_ADDRESS
. Otherwise, the get
operation is performed.
get
It is used from the EVM for looking up block hashes.
- Callers provide the block number they are querying in a big-endian encoding.
- If calldata is bigger than 2^64-1, revert.
- For any output outside the range of [block.number-
L2_HISTORY_BUFFER_LENGTH
, block.number-1] return 0.
set
- Caller provides
l2Block.parent.hash
as calldata to the contract. - Set the storage value at
block.number-1 % L2_HISTORY_BUFFER_LENGTH
to becalldata[0:32]
.
Bytecode
The exact contract bytecode is shared below.
Deployment
The L2 history storage contract can be deployed as a pre-deployed contract.
Block processing
At the top of every L2 block where block.timestamp >= L2_HISTORY_FORK_TIMESTAMP
the sequencer will call to
L2_HISTORY_STORAGE_ADDRESS
as L2_HISTORY_SYSTEM_ADDRESS
with the 32-byte input of l2Block.parent.hash
, a gas limit
of 30_000_000
, and 0
value. This will trigger the set()
routine of the history contract.
- the call must execute to completion
- the call does not count against the block's gas limit
- the call does not follow the EIP-1559 burn semantics - no value should be transferred as part of the call
- if no code exists at
L2_HISTORY_STORAGE_ADDRESS
, the call must fail silently
Note that, it will take L2_HISTORY_SYSTEM_ADDRESS
blocks after the EIP's activation to completely fill up the ring
buffer. Initially the contract will only contain the parent hash of the fork block and no hashes prior to that.
Rationale
Hash of a subset of execution block fields instead of beacon block root of full execution block hash
keccak256([l1Origin.block.height, l1Origin.block.stateRoot, l1Origin.block.transactionRoot, l1Origin.block.receiptRoot])
is committed into the ring buffer in order to reduce gas costs relative to using either beacon block roots or execution
block hashes.
The preimage to this hash is the 128 byte concatenation of the four fields
[l1Origin.block.height, l1Origin.block.stateRoot, l1Origin.block.transactionRoot, l1Origin.block.receiptRoot]
.
This is smaller than the 708 byte preimage of a full execution header and smaller than an SSZ proof for a Merkle tree
with depth of 3 or greater (160+ bytes).
The tradeoff is that the contents of this ring buffer can not be used to verify other fields of the L1 execution header or beacon block.
Length of the ring buffers
Ring buffer lengths are currently set to the lengths prescribed in EIP-2935 and EIP-4788.
For the L2 history storage contract 8192 L2 headers corresponds to ~4.55 hours worth of L2 blocks for an L2 with block time of 2 seconds or ~34 minutes worth of blocks for an L2 with block time of 250 milliseconds. The former is enough time to span the current average L2 output interval of major L2s today (~1 hour). The latter is not and warrants discussing an extension of the ring buffer to accommodate L2s with faster block times or longer output frequencies. For this reason it may make sense to not prescribe a set length for this ring buffer as L2s can set the length that best fits their system.
Backwards Compatibility
This RIP introduces backwards incompatible changes to the block derivation and verification of an L2. These changes are purely additive and do not break anything related to current user activity and experience, but they do require changes to the L2's L1->L2 derivation process and the verification logic in their output settlement contract(s).
Test Cases
N/A
Reference Implementation
N/A
Security Considerations
N/A
Copyright
Copyright and related rights waived via CC0.