eth2.0-specs/specs/core/0_fork-choice.md

339 lines
14 KiB
Markdown
Raw Normal View History

2019-04-22 13:20:48 +00:00
# Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice
2019-05-06 15:30:32 +00:00
**Notice**: This document is a work-in-progress for researchers and implementers.
2019-04-22 13:20:48 +00:00
## Table of contents
<!-- TOC -->
- [Ethereum 2.0 Phase 0 -- Beacon Chain Fork Choice](#ethereum-20-phase-0----beacon-chain-fork-choice)
- [Table of contents](#table-of-contents)
- [Introduction](#introduction)
2019-06-15 22:42:03 +00:00
- [Fork choice](#fork-choice)
2019-06-20 10:58:05 +00:00
- [Helpers](#helpers)
2019-06-30 20:57:42 +00:00
- [`LatestMessage`](#latestmessage)
2019-06-15 22:42:03 +00:00
- [`Store`](#store)
- [`get_genesis_store`](#get_genesis_store)
- [`get_ancestor`](#get_ancestor)
2019-06-20 10:58:05 +00:00
- [`get_latest_attesting_balance`](#get_latest_attesting_balance)
2019-06-15 22:42:03 +00:00
- [`get_head`](#get_head)
- [Handlers](#handlers)
- [`on_tick`](#on_tick)
- [`on_block`](#on_block)
- [`on_attestation`](#on_attestation)
2019-04-22 13:20:48 +00:00
<!-- /TOC -->
## Introduction
2019-06-15 22:42:03 +00:00
This document is the beacon chain fork choice spec, part of Ethereum 2.0 Phase 0. It assumes the [beacon chain state transition function spec](./0_beacon-chain.md).
2019-06-15 22:42:03 +00:00
## Fork choice
The head block root associated with a `store` is defined as `get_head(store)`. At genesis, let `store = get_genesis_store(genesis_state)` and update `store` by running:
2019-06-15 22:42:03 +00:00
2019-06-30 19:25:58 +00:00
- `on_tick(time)` whenever `time > store.time` where `time` is the current Unix time
- `on_block(block)` whenever a block `block` is received
- `on_attestation(attestation)` whenever an attestation `attestation` is received
2019-06-15 22:42:03 +00:00
*Notes*:
1) **Leap seconds**: Slots will last `SECONDS_PER_SLOT + 1` or `SECONDS_PER_SLOT - 1` seconds around leap seconds. This is automatically handled by [UNIX time](https://en.wikipedia.org/wiki/Unix_time).
2019-06-15 22:42:03 +00:00
2) **Honest clocks**: Honest nodes are assumed to have clocks synchronized within `SECONDS_PER_SLOT` seconds of each other.
3) **Eth1 data**: The large `ETH1_FOLLOW_DISTANCE` specified in the [honest validator document](../validator/0_beacon-chain-validator.md) should ensure that `state.latest_eth1_data` of the canonical Ethereum 2.0 chain remains consistent with the canonical Ethereum 1.0 chain. If not, emergency manual intervention will be required.
2019-06-20 10:58:05 +00:00
4) **Manual forks**: Manual forks may arbitrarily change the fork choice rule but are expected to be enacted at epoch transitions, with the fork details reflected in `state.fork`.
2019-06-21 17:18:24 +00:00
5) **Implementation**: The implementation found in this specification is constructed for ease of understanding rather than for optimization in computation, space, or any other resource. A number of optimized alternatives can be found [here](https://github.com/protolambda/lmd-ghost).
2019-06-15 22:42:03 +00:00
### Configuration
| Name | Value | Unit | Duration |
| - | - | :-: | :-: |
2019-11-05 19:51:47 +00:00
| `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds |
2019-06-20 10:58:05 +00:00
### Helpers
2019-04-22 13:20:48 +00:00
2019-06-25 20:42:37 +00:00
#### `LatestMessage`
```python
@dataclass(eq=True, frozen=True)
class LatestMessage(object):
epoch: Epoch
root: Root
2019-06-25 20:42:37 +00:00
```
2019-06-15 22:42:03 +00:00
#### `Store`
```python
@dataclass
class Store(object):
time: uint64
genesis_time: uint64
2019-06-25 03:01:15 +00:00
justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
best_justified_checkpoint: Checkpoint
blocks: Dict[Root, BeaconBlock] = field(default_factory=dict)
block_states: Dict[Root, BeaconState] = field(default_factory=dict)
2019-06-21 18:55:55 +00:00
checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict)
2019-06-25 20:42:37 +00:00
latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict)
2019-06-15 22:42:03 +00:00
```
2019-04-22 13:20:48 +00:00
2019-06-15 22:42:03 +00:00
#### `get_genesis_store`
2019-04-22 13:20:48 +00:00
2019-06-15 22:42:03 +00:00
```python
def get_genesis_store(genesis_state: BeaconState) -> Store:
genesis_block = BeaconBlock(state_root=hash_tree_root(genesis_state))
root = signing_root(genesis_block)
2019-06-25 03:43:05 +00:00
justified_checkpoint = Checkpoint(epoch=GENESIS_EPOCH, root=root)
finalized_checkpoint = Checkpoint(epoch=GENESIS_EPOCH, root=root)
return Store(
time=genesis_state.genesis_time,
genesis_time=genesis_state.genesis_time,
2019-06-21 18:55:55 +00:00
justified_checkpoint=justified_checkpoint,
finalized_checkpoint=finalized_checkpoint,
best_justified_checkpoint=justified_checkpoint,
2019-06-25 03:01:15 +00:00
blocks={root: genesis_block},
2019-06-25 17:48:55 +00:00
block_states={root: genesis_state.copy()},
2019-06-25 03:01:15 +00:00
checkpoint_states={justified_checkpoint: genesis_state.copy()},
)
2019-06-15 22:42:03 +00:00
```
2019-04-22 13:20:48 +00:00
2019-11-07 00:20:21 +00:00
#### `get_current_slot`
```python
def get_current_slot(store: Store) -> Slot:
return Slot((store.time - store.genesis_time) // SECONDS_PER_SLOT)
```
2019-11-07 00:20:21 +00:00
#### `compute_slots_since_epoch_start`
```python
def compute_slots_since_epoch_start(slot: Slot) -> int:
return slot - compute_start_slot_at_epoch(compute_epoch_at_slot(slot))
```
2019-06-15 22:42:03 +00:00
#### `get_ancestor`
2019-04-22 13:20:48 +00:00
2019-06-15 22:42:03 +00:00
```python
def get_ancestor(store: Store, root: Root, slot: Slot) -> Root:
2019-06-15 22:42:03 +00:00
block = store.blocks[root]
if block.slot > slot:
return get_ancestor(store, block.parent_root, slot)
elif block.slot == slot:
return root
else:
return Bytes32() # root is older than queried slot: no results.
2019-06-15 22:42:03 +00:00
```
2019-04-22 13:20:48 +00:00
2019-06-20 10:58:05 +00:00
#### `get_latest_attesting_balance`
2019-04-22 13:20:48 +00:00
```python
def get_latest_attesting_balance(store: Store, root: Root) -> Gwei:
2019-06-21 18:55:55 +00:00
state = store.checkpoint_states[store.justified_checkpoint]
2019-06-25 17:48:55 +00:00
active_indices = get_active_validator_indices(state, get_current_epoch(state))
return Gwei(sum(
2019-06-25 17:48:55 +00:00
state.validators[i].effective_balance for i in active_indices
2019-06-30 20:59:12 +00:00
if (i in store.latest_messages
and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root)
))
2019-04-22 13:20:48 +00:00
```
2019-11-25 22:06:33 +00:00
#### `filter_block_tree`
```python
def filter_block_tree(store: Store, block_root: Root, blocks: Dict[Root, BeaconBlock]) -> bool:
2019-11-25 22:06:33 +00:00
block = store.blocks[block_root]
children = [
root for root in store.blocks.keys()
if store.blocks[root].parent_root == block_root
2019-11-25 22:06:33 +00:00
]
# If any children branches contain expected finalized/justified checkpoints,
# add to filtered block-tree and signal viability to parent.
if any(children):
if True in [filter_block_tree(store, child, blocks) for child in children]:
2019-11-25 22:06:33 +00:00
blocks[block_root] = block
return True
return False
# If leaf block, check finalized/justified checkpoints as matching latest.
# If matching, add to viable block-tree and signal viability to parent.
head_state = store.block_states[block_root]
# Handle base case where justified hasn't updated yet
if head_state.current_justified_checkpoint.epoch == GENESIS_EPOCH:
is_viable_branch = True
else:
is_viable_branch = (
head_state.finalized_checkpoint == store.finalized_checkpoint
and head_state.current_justified_checkpoint == store.justified_checkpoint
)
2019-11-25 22:06:33 +00:00
if is_viable_branch:
blocks[block_root] = block
return True
# Otherwise, not viable
return False
```
#### `get_filtered_block_tree`
```python
def get_filtered_block_tree(store: Store) -> Dict[Root, BeaconBlock]:
head = store.justified_checkpoint.root
blocks: Dict[Root, BeaconBlock] = {}
filter_block_tree(store, head, blocks)
2019-11-25 22:06:33 +00:00
return blocks
```
2019-06-15 22:42:03 +00:00
#### `get_head`
2019-04-22 13:20:48 +00:00
```python
def get_head(store: Store) -> Root:
2019-11-25 22:06:33 +00:00
# Get filtered block tree that includes viable branches
blocks = get_filtered_block_tree(store)
print(len(blocks))
print(len(store.blocks))
2019-06-15 22:42:03 +00:00
# Execute the LMD-GHOST fork choice
2019-06-21 18:55:55 +00:00
head = store.justified_checkpoint.root
2019-10-23 00:37:15 +00:00
justified_slot = compute_start_slot_at_epoch(store.justified_checkpoint.epoch)
2019-06-15 22:42:03 +00:00
while True:
children = [
2019-11-25 22:06:33 +00:00
root for root in blocks.keys()
if blocks[root].parent_root == head and blocks[root].slot > justified_slot
]
2019-04-22 13:20:48 +00:00
if len(children) == 0:
return head
2019-06-20 10:58:05 +00:00
# Sort by latest attesting balance with ties broken lexicographically
head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root))
2019-06-15 22:42:03 +00:00
```
#### `should_update_justified_checkpoint`
```python
def should_update_justified_checkpoint(store: Store, new_justified_checkpoint: Checkpoint) -> bool:
2019-11-07 18:51:53 +00:00
"""
To address the bouncing attack, only update conflicting justified
checkpoints in the fork choice if in the early slots of the epoch.
Otherwise, delay incorporation of new justified checkpoint until next epoch boundary.
See https://ethresear.ch/t/prevention-of-bouncing-attack-on-ffg/6114 for more detailed analysis and discussion.
"""
2019-11-07 00:20:21 +00:00
if compute_slots_since_epoch_start(get_current_slot(store)) < SAFE_SLOTS_TO_UPDATE_JUSTIFIED:
return True
new_justified_block = store.blocks[new_justified_checkpoint.root]
if new_justified_block.slot <= compute_start_slot_at_epoch(store.justified_checkpoint.epoch):
return False
if not (
get_ancestor(store, new_justified_checkpoint.root, store.blocks[store.justified_checkpoint.root].slot) ==
store.justified_checkpoint.root
):
return False
return True
```
2019-06-15 22:42:03 +00:00
### Handlers
#### `on_tick`
```python
def on_tick(store: Store, time: uint64) -> None:
previous_slot = get_current_slot(store)
# update store time
2019-06-15 22:42:03 +00:00
store.time = time
current_slot = get_current_slot(store)
# Not a new epoch, return
2019-11-07 18:51:53 +00:00
if not (current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0):
return
2019-11-07 18:51:53 +00:00
# Update store.justified_checkpoint if a better checkpoint is known
if store.best_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
store.justified_checkpoint = store.best_justified_checkpoint
2019-04-22 13:20:48 +00:00
```
2019-06-15 22:42:03 +00:00
#### `on_block`
2019-06-15 22:42:03 +00:00
```python
def on_block(store: Store, block: BeaconBlock) -> None:
2019-06-21 11:00:42 +00:00
# Make a copy of the state to avoid mutability issues
2019-06-25 03:09:57 +00:00
assert block.parent_root in store.block_states
2019-06-25 03:01:15 +00:00
pre_state = store.block_states[block.parent_root].copy()
2019-06-21 10:19:08 +00:00
# Blocks cannot be in the future. If they are, their consideration must be delayed until the are in the past.
assert store.time >= pre_state.genesis_time + block.slot * SECONDS_PER_SLOT
2019-06-15 22:42:03 +00:00
# Add new block to the store
store.blocks[signing_root(block)] = block
# Check block is a descendant of the finalized block
2019-06-25 03:09:57 +00:00
assert (
get_ancestor(store, signing_root(block), store.blocks[store.finalized_checkpoint.root].slot) ==
store.finalized_checkpoint.root
)
# Check that block is later than the finalized epoch slot
2019-10-23 00:37:15 +00:00
assert block.slot > compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
2019-06-15 22:42:03 +00:00
# Check the block is valid and compute the post-state
state = state_transition(pre_state, block, True)
# Add new state for this block to the store
2019-06-21 18:55:55 +00:00
store.block_states[signing_root(block)] = state
# Update justified checkpoint
2019-06-25 03:43:05 +00:00
if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
store.best_justified_checkpoint = state.current_justified_checkpoint
if should_update_justified_checkpoint(store, state.current_justified_checkpoint):
store.justified_checkpoint = state.current_justified_checkpoint
2019-06-21 18:55:55 +00:00
# Update finalized checkpoint
2019-06-25 03:43:05 +00:00
if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch:
store.finalized_checkpoint = state.finalized_checkpoint
2019-06-15 22:42:03 +00:00
```
2019-06-15 22:42:03 +00:00
#### `on_attestation`
```python
def on_attestation(store: Store, attestation: Attestation) -> None:
2019-11-12 23:24:33 +00:00
"""
Run ``on_attestation`` upon receiving a new ``attestation`` from either within a block or directly on the wire.
An ``attestation`` that is asserted as invalid may be valid at a later time,
consider scheduling it for later processing in such case.
"""
2019-06-25 03:43:05 +00:00
target = attestation.data.target
2019-06-21 18:55:55 +00:00
# Attestations must be from the current or previous epoch
current_epoch = compute_epoch_at_slot(get_current_slot(store))
# Use GENESIS_EPOCH for previous when genesis to avoid underflow
previous_epoch = current_epoch - 1 if current_epoch > GENESIS_EPOCH else GENESIS_EPOCH
assert target.epoch in [current_epoch, previous_epoch]
2019-06-21 18:55:55 +00:00
# Cannot calculate the current shuffling if have not seen the target
2019-06-24 23:18:22 +00:00
assert target.root in store.blocks
# Attestations target be for a known block. If target block is unknown, delay consideration until the block is found
assert target.root in store.blocks
2019-06-30 19:25:58 +00:00
# Attestations cannot be from future epochs. If they are, delay consideration until the epoch arrives
2019-06-25 03:01:15 +00:00
base_state = store.block_states[target.root].copy()
2019-10-23 00:37:15 +00:00
assert store.time >= base_state.genesis_time + compute_start_slot_at_epoch(target.epoch) * SECONDS_PER_SLOT
# Attestations must be for a known block. If block is unknown, delay consideration until the block is found
assert attestation.data.beacon_block_root in store.blocks
# Attestations must not be for blocks in the future. If not, the attestation should not be considered
assert store.blocks[attestation.data.beacon_block_root].slot <= attestation.data.slot
2019-06-21 18:55:55 +00:00
# Store target checkpoint state if not yet seen
2019-06-24 23:18:22 +00:00
if target not in store.checkpoint_states:
2019-10-23 00:37:15 +00:00
process_slots(base_state, compute_start_slot_at_epoch(target.epoch))
2019-06-25 03:09:57 +00:00
store.checkpoint_states[target] = base_state
2019-06-24 23:18:22 +00:00
target_state = store.checkpoint_states[target]
# Attestations can only affect the fork choice of subsequent slots.
# Delay consideration in the fork choice until their slot is in the past.
2019-10-18 03:10:36 +00:00
assert store.time >= (attestation.data.slot + 1) * SECONDS_PER_SLOT
2019-06-24 23:18:22 +00:00
# Get state at the `target` to validate attestation and calculate the committees
2019-06-30 09:02:18 +00:00
indexed_attestation = get_indexed_attestation(target_state, attestation)
assert is_valid_indexed_attestation(target_state, indexed_attestation)
2019-06-25 20:42:37 +00:00
# Update latest messages
2019-11-02 03:02:53 +00:00
for i in indexed_attestation.attesting_indices:
2019-06-25 20:42:37 +00:00
if i not in store.latest_messages or target.epoch > store.latest_messages[i].epoch:
store.latest_messages[i] = LatestMessage(epoch=target.epoch, root=attestation.data.beacon_block_root)
2019-06-15 22:42:03 +00:00
```