# `get_domain` for very old epochs This document aims to illuminate mismatching interpretations of the Ethereum Proof-of-Stake Consensus Specification's phase0 [get_domain](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_domain) function. The two different interpretations result in mismatching signatures. Author: corver@obol.tech Date: 23 Aug 2022 ## Interpretation A The spec defines the `get_domain` function as: ``` def get_domain(state: BeaconState, domain_type: DomainType, epoch: Epoch=None) -> Domain: """ Return the signature domain (fork version concatenated with domain type) of a message. """ epoch = get_current_epoch(state) if epoch is None else epoch fork_version = state.fork.previous_version if epoch < state.fork.epoch else state.fork.current_version return compute_domain(domain_type, fork_version, state.genesis_validators_root)``` ``` Interpretation A notes that `fork_version` is either the `previous_version` or `current_version` of the **current fork** regardless of whether the epoch is less than the epoch of an **earlier fork**. So given a Prater's current fork schedule of: ``` { "data": [ { "previous_version": "0x00001020", "current_version": "0x00001020", "epoch": "0" }, { "previous_version": "0x00001020", "current_version": "0x01001020", "epoch": "36660" }, { "previous_version": "0x01001020", "current_version": "0x02001020", "epoch": "112260" } ] } ``` The value of `state.fork` will be the last value in that array: ``` { "previous_version": "0x01001020", "current_version": "0x02001020", "epoch": "112260" } ``` Calculating `get_domain` would then result in the following: ``` # epoch >= 112260 --> fork_version=bellatrix epoch=112261 --> fork_version=0x02001020 epoch=112260 --> fork_version=0x02001020 # epoch < 112260 --> fork_version=altair epoch=112259 --> fork_version=0x01001020 epoch=36660 --> fork_version=0x01001020 epoch=36659 --> fork_version=0x01001020 # Even if epoch is from N-2 forks ago epoch=1 --> fork_version=0x01001020 ``` ### Client implementations using interpretation A - Lighthouse: - `get_domain`: takes a single `fork` along with other parameters, [see source](https://github.com/sigp/lighthouse/blob/stable/consensus/types/src/chain_spec.rs#L343-L352). - `fork.get_fork_version(epoch)`: returns either `previous_version` or `current_version`, [see source](https://github.com/sigp/lighthouse/blob/stable/consensus/types/src/fork.rs#L38-L43). - `spec.get_domain`: the fork passed to `get_domain` is contextual to the type of processing being performed. When doing block processing, the **head fork** is used. When doing historical processing, the historical **fork_at_epoch** is used. ### Implication of interpretation A The use-case of generating a [VoluntaryExit](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#voluntaryexit) at validator start-of-life and persisting it off-chain to be broadcasted at a later stage is not supported since the signature will not be valid after 2 subsequent forks. E.g. the signature of a `VoluntaryExit` generated during phase0 on Prater is not valid after the Bellatrix hard fork. ## Interpretation B Interpretation B could define the `get_domain` function slightly differently as: ``` def get_domain(state: BeaconState, domain_type: DomainType, epoch: Epoch=None) -> Domain: """ Return the signature domain (fork version concatenated with domain type) of a message. """ epoch = get_current_epoch(state) if epoch is None else epoch fork = fork_at_epoch(state, epoch) # Note that fork is not state.fork fork_version = fork.previous_version if epoch < fork.epoch else fork.current_version return compute_domain(domain_type, fork_version, state.genesis_validators_root)``` ``` Interpretation B uses **fork_at_epoch** logic to select the fork instead of using only the **current fork**. The `fork_at_epoch` function looks back into the fork schedule history and returns the active fork at the provided epoch. So given a Prater's current fork schedule mentioned above, calculating `get_domain` would then result in the following: ``` # epoch >= 112260 --> fork_version=bellatrix epoch=112261 --> fork_version=0x02001020 epoch=112260 --> fork_version=0x02001020 # 36660 <= epoch < 112260 --> fork_version=altair epoch=112259 --> fork_version=0x01001020 epoch=36660 --> fork_version=0x01001020 # epoch < 36660 --> fork_version=phase0 epoch=36659 --> fork_version=0x00001020 epoch=1 --> fork_version=0x00001020 ``` ### Client implementations using interpretation B - Vouch: - Uses go-eth2-client library, [see source](https://github.com/attestantio/go-eth2-client/blob/master/http/domain.go#L27). - Prysm: - gRPC endpoint called `DomainData`, [see source](https://github.com/prysmaticlabs/prysm/blob/develop/beacon-chain/rpc/prysm/v1alpha1/validator/server.go#L136) - Which calls `fork.Fork` that does the `fork_at_epoch` logic, [see source](https://github.com/prysmaticlabs/prysm/blob/develop/network/forks/fork.go#L89) - Nimbus: - Uses `forkAtEpoch` to get the fork passed into validation/signing logic, [see source](https://github.com/status-im/nimbus-eth2/blob/stable/beacon_chain/gossip_processing/gossip_validation.nim#L488) - Lodestar - Uses `startSlotAtEpoch` to get a slot that is passed into `get_domain`, [see source](computeStartSlotAtEpoch). - Uses `getForkInfo` which is equivalent to `fork_at_epoch`, [see source](https://github.com/ChainSafe/lodestar/blob/unstable/packages/config/src/genesisConfig/index.ts#L38). ## Risk of mismatching interpretations Any block containing a `VoluntaryExit` with a very old epoch (for fork N-2) will result in a chain split since the verification of its signature by the two different interpretations are incompatble. Example: - A Teku client produces a block with a `VoluntaryExit` with epoch=1. - Other interpretation B clients will accept it, attest it and include that block in their state. - Lighthouse clients will see the `VoluntaryExit` signature as invalid and therefore the block as invalid so will not include it in its state.