-
Notifications
You must be signed in to change notification settings - Fork 998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Execution-ified fork choice, draft 1 #1068
Changes from all commits
0642af1
68d555f
333a08e
847c554
7c8565f
b136ced
c24218f
b78bbd1
275c90c
27e3651
cb3522b
27eaa66
9e9fbf2
349f80f
1299992
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,23 +8,27 @@ | |
- [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) | ||
- [Prerequisites](#prerequisites) | ||
- [Constants](#constants) | ||
- [Time parameters](#time-parameters) | ||
- [Beacon chain processing](#beacon-chain-processing) | ||
- [Beacon chain fork choice rule](#beacon-chain-fork-choice-rule) | ||
- [Implementation notes](#implementation-notes) | ||
- [Justification and finality at genesis](#justification-and-finality-at-genesis) | ||
- [Fork choice](#fork-choice) | ||
- [Containers](#containers) | ||
- [`Target`](#target) | ||
- [`Store`](#store) | ||
- [Helpers](#helpers) | ||
- [`get_genesis_store`](#get_genesis_store) | ||
- [`get_ancestor`](#get_ancestor) | ||
- [`get_attesting_balance_from_store`](#get_attesting_balance_from_store) | ||
- [`get_head`](#get_head) | ||
- [Handlers](#handlers) | ||
- [`on_tick`](#on_tick) | ||
- [`on_block`](#on_block) | ||
- [`on_attestation`](#on_attestation) | ||
|
||
<!-- /TOC --> | ||
|
||
## Introduction | ||
|
||
This document represents the specification for the beacon chain fork choice rule, part of Ethereum 2.0 Phase 0. | ||
|
||
## Prerequisites | ||
|
||
All terminology, constants, functions, and protocol mechanics defined in the [Phase 0 -- The Beacon Chain](./0_beacon-chain.md) doc are requisite for this document and used throughout. Please see the Phase 0 doc before continuing and use as a reference throughout. | ||
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). | ||
|
||
## Constants | ||
|
||
|
@@ -34,76 +38,132 @@ All terminology, constants, functions, and protocol mechanics defined in the [Ph | |
| - | - | :-: | :-: | | ||
| `SECONDS_PER_SLOT` | `6` | seconds | 6 seconds | | ||
|
||
## Beacon chain processing | ||
## 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: | ||
|
||
* `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 | ||
|
||
*Notes*: | ||
|
||
1) **Leap seconds**: Slots will last `SECONDS_PER_SLOT + 1` or `SECONDS_PER_SLOT - 1` seconds around leap seconds. | ||
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](https://github.com/ethereum/eth2.0-specs/blob/dev/specs/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. | ||
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`. | ||
|
||
### Containers | ||
|
||
Processing the beacon chain is similar to processing the Ethereum 1.0 chain. Clients download and process blocks and maintain a view of what is the current "canonical chain", terminating at the current "head". For a beacon block, `block`, to be processed by a node, the following conditions must be met: | ||
#### `Target` | ||
|
||
```python | ||
@dataclass | ||
class Target: | ||
epoch: Epoch | ||
root: Bytes32 | ||
``` | ||
|
||
#### `Store` | ||
|
||
```python | ||
@dataclass | ||
class Store: | ||
blocks: Dict[Bytes32, BeaconBlock] = field(default_factory=dict) | ||
states: Dict[Bytes32, BeaconState] = field(default_factory=dict) | ||
time: int = 0 | ||
latest_targets: Dict[ValidatorIndex, Target] = field(default_factory=dict) | ||
justified_root: Bytes32 = ZERO_HASH | ||
finalized_root: Bytes32 = ZERO_HASH | ||
``` | ||
|
||
* The parent block with root `block.parent_root` has been processed and accepted. | ||
* An Ethereum 1.0 block pointed to by the `state.latest_eth1_data.block_hash` has been processed and accepted. | ||
* The node's Unix time is greater than or equal to `state.genesis_time + block.slot * SECONDS_PER_SLOT`. | ||
### Helpers | ||
|
||
*Note*: Leap seconds mean that slots will occasionally last `SECONDS_PER_SLOT + 1` or `SECONDS_PER_SLOT - 1` seconds, possibly several times a year. | ||
#### `get_genesis_store` | ||
|
||
*Note*: Nodes needs to have a clock that is roughly (i.e. within `SECONDS_PER_SLOT` seconds) synchronized with the other nodes. | ||
```python | ||
def get_genesis_store(genesis_state: BeaconState) -> Store: | ||
genesis_block = BeaconBlock(state_root=hash_tree_root(genesis_state)) | ||
root = signing_root(genesis_block) | ||
return Store(blocks={root: genesis_block}, states={root: genesis_state}, finalized_root=root, justified_root=root) | ||
``` | ||
|
||
### Beacon chain fork choice rule | ||
#### `get_ancestor` | ||
|
||
The beacon chain fork choice rule is a hybrid that combines justification and finality with Latest Message Driven (LMD) Greediest Heaviest Observed SubTree (GHOST). At any point in time, a validator `v` subjectively calculates the beacon chain head as follows. | ||
```python | ||
def get_ancestor(store: Store, root: Bytes32, slot: Slot) -> Bytes32: | ||
block = store.blocks[root] | ||
assert block.slot >= slot | ||
return root if block.slot == slot else get_ancestor(store, block.parent_root, slot) | ||
``` | ||
|
||
* Abstractly define `Store` as the type of storage object for the chain data, and let `store` be the set of attestations and blocks that the validator `v` has observed and verified (in particular, block ancestors must be recursively verified). Attestations not yet included in any chain are still included in `store`. | ||
* Let `finalized_head` be the finalized block with the highest epoch. (A block `B` is finalized if there is a descendant of `B` in `store`, the processing of which sets `B` as finalized.) | ||
* Let `justified_head` be the descendant of `finalized_head` with the highest epoch that has been justified for at least 1 epoch. (A block `B` is justified if there is a descendant of `B` in `store` the processing of which sets `B` as justified.) If no such descendant exists, set `justified_head` to `finalized_head`. | ||
* Let `get_ancestor(store: Store, block: BeaconBlock, slot: Slot) -> BeaconBlock` be the ancestor of `block` with slot number `slot`. The `get_ancestor` function can be defined recursively as: | ||
#### `get_attesting_balance_from_store` | ||
|
||
```python | ||
def get_ancestor(store: Store, block: BeaconBlock, slot: Slot) -> BeaconBlock: | ||
""" | ||
Get the ancestor of ``block`` with slot number ``slot``; return ``None`` if not found. | ||
""" | ||
if block.slot == slot: | ||
return block | ||
elif block.slot < slot: | ||
return None | ||
else: | ||
return get_ancestor(store, store.get_parent(block), slot) | ||
def get_attesting_balance_from_store(store: Store, root: Bytes32) -> Gwei: | ||
state = store.states[store.justified_root] | ||
active_indices = get_active_validator_indices(state.validator_registry, slot_to_epoch(state.slot)) | ||
return sum( | ||
state.validator_registry[i].effective_balance for i in active_indices | ||
if get_ancestor(store, store.latest_targets[i].root, store.blocks[root].slot) == root | ||
) | ||
``` | ||
|
||
* Let `get_latest_attestation(store: Store, index: ValidatorIndex) -> Attestation` be the attestation with the highest slot number in `store` from the validator with the given `index`. If several such attestations exist, use the one the validator `v` observed first. | ||
* Let `get_latest_attestation_target(store: Store, index: ValidatorIndex) -> BeaconBlock` be the target block in the attestation `get_latest_attestation(store, index)`. | ||
* Let `get_children(store: Store, block: BeaconBlock) -> List[BeaconBlock]` return the child blocks of the given `block`. | ||
* Let `justified_head_state` be the resulting `BeaconState` object from processing the chain up to the `justified_head`. | ||
* The `head` is `lmd_ghost(store, justified_head_state, justified_head)` where the function `lmd_ghost` is defined below. Note that the implementation below is suboptimal; there are implementations that compute the head in time logarithmic in slot count. | ||
#### `get_head` | ||
|
||
```python | ||
def lmd_ghost(store: Store, start_state: BeaconState, start_block: BeaconBlock) -> BeaconBlock: | ||
""" | ||
Execute the LMD-GHOST algorithm to find the head ``BeaconBlock``. | ||
""" | ||
validators = start_state.validator_registry | ||
active_validator_indices = get_active_validator_indices(validators, slot_to_epoch(start_state.slot)) | ||
attestation_targets = [(i, get_latest_attestation_target(store, i)) for i in active_validator_indices] | ||
|
||
# Use the rounded-balance-with-hysteresis supplied by the protocol for fork | ||
# choice voting. This reduces the number of recomputations that need to be | ||
# made for optimized implementations that precompute and save data | ||
def get_vote_count(block: BeaconBlock) -> int: | ||
return sum( | ||
start_state.validator_registry[validator_index].effective_balance | ||
for validator_index, target in attestation_targets | ||
if get_ancestor(store, target, block.slot) == block | ||
) | ||
|
||
head = start_block | ||
while 1: | ||
children = get_children(store, head) | ||
def get_head(store: Store) -> Bytes32: | ||
# Execute the LMD-GHOST fork choice | ||
head = store.justified_root | ||
while True: | ||
children = [root for root in store.blocks.keys() if store.blocks[root].parent_root == head] | ||
if len(children) == 0: | ||
return head | ||
# Ties broken by favoring block with lexicographically higher root | ||
head = max(children, key=lambda x: (get_vote_count(x), hash_tree_root(x))) | ||
# Sort by attesting balance with ties broken lexicographically | ||
head = max(children, key=lambda root: (get_attesting_balance_from_store(store, root), root)) | ||
``` | ||
|
||
### Handlers | ||
|
||
#### `on_tick` | ||
|
||
```python | ||
def on_tick(store: Store, time: int) -> None: | ||
store.time = time | ||
``` | ||
|
||
## Implementation notes | ||
#### `on_block` | ||
|
||
### Justification and finality at genesis | ||
```python | ||
def on_block(store: Store, block: BeaconBlock) -> None: | ||
# Add new block to the store | ||
store.blocks[signing_root(block)] = block | ||
# Check block is a descendant of the finalized block | ||
assert get_ancestor(store, signing_root(block), store.blocks[store.finalized_root].slot) == store.finalized_root | ||
# Check block slot against Unix time | ||
pre_state = store.states[block.parent_root].copy() | ||
assert store.time >= pre_state.genesis_time + block.slot * SECONDS_PER_SLOT | ||
# Check the block is valid and compute the post-state | ||
state = state_transition(pre_state, block) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahhhh a mutability issue here! 💣
A quick fix for this PR might still be adding the scary But open to continue discussing in #1059. |
||
# Add new state to the store | ||
store.states[signing_root(block)] = state | ||
# Update justified and finalized blocks | ||
if state.finalized_epoch > slot_to_epoch(store.blocks[store.finalized_root].slot): | ||
store.finalized_root = state.finalized_root | ||
if state.current_justified_epoch > slot_to_epoch(store.blocks[store.justified_root].slot): | ||
store.justified_root = state.current_justified_root | ||
if state.previous_justified_epoch > slot_to_epoch(store.blocks[store.justified_root].slot): | ||
store.justified_root = state.previous_justified_root | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggestion: if state.current_justified_epoch > slot_to_epoch(store.blocks[store.justified_root].slot):
store.justified_root = state.current_justified_root
elif state.previous_justified_epoch > slot_to_epoch(store.blocks[store.justified_root].slot):
store.justified_root = state.previous_justified_root |
||
``` | ||
|
||
During genesis, justification and finality root fields within the `BeaconState` reference `ZERO_HASH` rather than a known block. `ZERO_HASH` in `previous_justified_root`, `current_justified_root`, and `finalized_root` should be considered as an alias to the root of the genesis block. | ||
#### `on_attestation` | ||
|
||
```python | ||
def on_attestation(store: Store, attestation: Attestation) -> None: | ||
state = store.states[get_head(store)] | ||
indexed_attestation = convert_to_indexed(state, attestation) | ||
validate_indexed_attestation(state, indexed_attestation) | ||
for i in indexed_attestation.custody_bit_0_indices + indexed_attestation.custody_bit_1_indices: | ||
if i not in store.latest_targets or attestation.data.target_epoch > store.latest_targets[i].epoch: | ||
store.latest_targets[i] = Target(attestation.data.target_epoch, attestation.data.target_root) | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,4 @@ eth-utils>=1.3.0,<2 | |
eth-typing>=2.1.0,<3.0.0 | ||
pycryptodome==3.7.3 | ||
py_ecc>=1.6.0 | ||
dataclasses==0.6 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The given
block
hasn't been stored yet here, so insideget_ancestor
, it can't access the givenblock
fromstore
.As a beacon node, I support it needs to store the block before these verifications?