# `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.