---
# System prepended metadata

title: '`get_domain` for very old epochs'

---

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