--- tags: hard_fork_1 --- # Hard forking Beacon State ## Goal * Background * What issues do we need to solve? * What solutions do we have? ## Background Hardfork 1 is coming this summer. One of the biggest foreseeable issue is there will be two versions of beacon state. One for pre fork and one for post fork. Everything that uses the beacon state will have to adapt to the two variations. Example: ``` Before epoch N: use beacon state v1 After epoch N: use beacon state v2 ``` Such changes if not planned carefully will become technical debts and cause lingering issues throughout Prysm's life time. Assuming there's one hard fork per year, this topic is something we'll revisit time and time, it's important that we document our thoughts, feedbacks and solutions during this process. ## Refresher Currently beacon state looks like: ```python= class BeaconState(Container): # Versioning genesis_time: uint64 genesis_validators_root: Root slot: Slot fork: Fork # History latest_block_header: BeaconBlockHeader block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] # Eth1 eth1_data: Eth1Data eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index: uint64 # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] # Randomness randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] # Slashings slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances # Attestations previous_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] current_epoch_attestations: List[PendingAttestation, MAX_ATTESTATIONS * SLOTS_PER_EPOCH] # Finality justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # Bit set for every recent justified epoch previous_justified_checkpoint: Checkpoint # Previous epoch snapshot current_justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint ``` After hard fork, the beacon state will looke like: ```python= class BeaconState(Container): # Versioning genesis_time: uint64 genesis_validators_root: Root slot: Slot fork: Fork # History latest_block_header: BeaconBlockHeader block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] # Eth1 eth1_data: Eth1Data eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index: uint64 # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] # Randomness randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] # Slashings slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances # Participation previous_epoch_participation: List[ValidatorFlag, VALIDATOR_REGISTRY_LIMIT] current_epoch_participation: List[ValidatorFlag, VALIDATOR_REGISTRY_LIMIT] # Finality justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # Bit set for every recent justified epoch previous_justified_checkpoint: Checkpoint current_justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint # Light client sync committees current_sync_committee: SyncCommittee next_sync_committee: SyncCommittee ``` As we can see, there's going to be *replacement* and *extension*. * `current/previous_epoch_attestations` which is `List[PendingAttestation]` is getting replaced by `current/previous_epoch_participation` which is `List[ValidatorFlag]` * The beacon state is also getting extended by new fields such as `current_sync_committee` and `next_sync_committee` For designing, we should aim to design for both cases that is `replacement` and `extension` ## Solution The most ideal solution is to implement a layer of abstraction with some wrappers around the two beacon state types. Since protobuf has getter/setter methods, we could use an interface which both states conform to. (e.g. `BeaconState` interface) We want to avoid: `db.SaveStateV1(*BeaconStateV1)` and `db.SaveStateV2(*BeaconStateV2)` We want `db.SaveState(BeaconState)` where `BeaconState` is an interface: ```go type BeaconState interface { Validators() []*ethpb.Validator ... } type BeaconStateV2 interface { BeaconState Eth1State() *ethpb.Eth1State // some new methods/fields added in the hard fork } ``` Within `SaveState`, the function can use reflect and detect whether it's v1 or v2 then save it to the appropiate bucket. ## Affected area `BeaconState` is heavily used across Prysm, this will impact not only in DB but in every service and every storage unit. Some notable ones: * Core processing block using parent state * Sync receiving beacon chain objects * RPC server for serving validators * RPC server for serving beacon block explorers * Database for storage * Cache for storage ## Side note `BeaconBlockBody` will also be different post-hardfork. Whichever mechanism that came up with here, it should be reusable for the others. ## Proposal 1.) First, let's choose the location. I'm putting it under`beacon-chain/state/types` I think this is the best location given beacon state is not used in other pkgs such as `validator`, `slasher` and `shared`. It also lives next to it's implementation `beacon-chain/state`. 2.) We'll use folders `v0`, `v1`... to distingush beacon state internal changes due to network upgrade aka. hard fork. 3.) Each version `v0`, `v1`... has its own unique `struct.go` ```go= package v0 type BeaconState struct { GenesisTime uint64 Slot types.Slot GenesisValidatorsRoot [32]byte BlockRoots [][32]byte Validators []*v1alpha1.Validator PreviousJustifiedCheckpoint *v1alpha1.Checkpoint } ``` 4.) The `BeaconState` from above implements the interface that's defined in `interface.go` under `beacon-chain/state/types`. Which looks like: ```go= package types type BeaconStateVO interface { GenesisTime() uint64 SetGenesisTime(time uint64) } type BeaconStateV1 interface { BeaconStateVO // Post HF1 state getters and setters. } ``` 5.) In `beacon-chain/state/` pkg, we replace pb with the interface ```go= type BeaconState struct { state *pbp2p.BeaconState lock sync.RWMutex dirtyFields map[fieldIndex]interface{} ... ``` ```go= type BeaconState struct { state stateTypes.BeaconStateVO lock sync.RWMutex dirtyFields map[fieldIndex]interface{} ... ``` 6.) Then we'll replace everywhere that was using pb's getter/setter ```go= func (b *BeaconState) genesisTime() uint64 { if !b.HasInnerState() { return 0 } return b.state.GenesisTime } ``` ```go= func (b *BeaconState) genesisTime() uint64 { if !b.HasInnerState() { return 0 } return b.state.GenesisTime() } ``` The project structure looks like the following: ![](https://i.imgur.com/Et7eVoC.png =200x500) ## Open questions 1.) For root are we using []byte or [32]byte? ```go= type BeaconState struct { GenesisValidatorsRoot [32]byte //or []byte BlockRoots [][32]byte //or [][]byte } ``` 2.) What do we do with function that returns the underlying beacon state pb object? Do we just return the underlying native struct object? ```go= // InnerStateUnsafe returns the pointer value of the underlying // beacon state proto object, bypassing immutability. Use with care. func (b *BeaconState) InnerStateUnsafe() *pbp2p.BeaconState { if b == nil { return nil } return b.state } ```