There are two groups of EIPs that affect the CL.
For supporting for CL datastructures (EIP-7688), the implementation burden is fairly low. There's no need to fully implement "full" StableContainer and "full" Variant support as specified, rather we only need to implement a subset of Variant. This simplified Variant has nearly identical functionality as Container, the only difference being a slightly modified hashTreeRoot.
For supporting new EL datastructures, implementation burden is higher. "Full" support for StableContainer is required.
Goal: explain the StableContainer EIP step-by-step with an emphasis on EIP-7688
EIP-7495 defines newStableContainer
and Variant
SSZ primitives.
The primary mechanism for providing "stability" (stability of merkleization) is the use of an "active fields" bitvector. Fields that are in use are toggled on, and fields that are not in use are toggled off. A StableContainer
is also defined with a maximum number of fields. As opposed to EIP-6475 - OptionalType, which wraps each type individually, StableContainer
uses the "active fields" bitvector to manage whether its fields are Some or None.
Merkleization is similar to Container
taking into account the active fields. Fields are merkleized at the depth determined by the maximum number of fields. Inactive fields are merkleized as 0x0, and active fields are merkleized normally. Then, the active fields bitvector is "mixed in" to the root, similarly to how the length is mixed-in to an SSZ List
.
Small example:
class Example(StableContainer[4]):
a: Optional<uint64>
b: Optional<uint64>
merkleized as
*
/ \
* 0b11 + 0x0
/
*
/ \
a b
Serialization is similar to Container
taking into account the active fields. The active field bitvector is prepended. Then, the fields are serialized like aContainer
, with the exception that inactive fields are skipped over entirely.
For the purposes of EIP-7688, what we've been calling a StableContainer
is actually a StableContainer
Variant
. "Lets turn the BeaconState into a StableContainer Variant"
StableContainer
can be thought of an abstract data type that exists across forks. It is a template for merkleization. It is not meant to be used directly by the state transition, networking, etc. Rather, we will be operating on specific Variant
s of StableContainer
s.
A Variant
of a StableContainer
is a specific subset of fields from the StableContainer
. It forgoes stable serialization (across variants) in favor of serialization rules that make it compatible with normal Container
serialization. Merkleization still includes the active fields bitvector.
Variant
s are useful for enhanced type safety and simpler (and slightly cheaper) serialization.
For example, for EIP-7688, we define a StableContainer
for the beacon state, lets call it AbstractBeaconState
, which defines the beacon state shape across all forks, electra onwards. Then we define fork-specific BeaconStateElectra
which only contains the fields relevant to that fork. This will be what we operate on in the spec and likely in our code.
At each future fork that updates the beacon state fields, either adds or deprecates a field, AbstractBeaconState
is updated with new fields, and a new Variant
will be defined that only includes the relevant fields.
Within the beacon node, AbstractBeaconState
would solely be used as a merkleization template. All beacon state variants would use the layout described by AbstractBeaconState
for merkleization. For example, BeaconStateElectra
is merkleized as an AbstractBeaconState
.
This explanation only treats a subset of features of StableContainer
and Variant
tailored for EIP-7688. Specifically it ignores handling of optional fields in both StableContainer
and Variant
.
Variant
as described above (ignoring potential field reordering during merkleization and also ignoring optional fields)