Skip to content
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

Closed
wants to merge 15 commits into from
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ phase0: $(PY_SPEC_PHASE_0_TARGETS)


$(PY_SPEC_DIR)/eth2spec/phase0/spec.py:
python3 $(SCRIPT_DIR)/phase0/build_spec.py $(SPEC_DIR)/core/0_beacon-chain.md $@
python3 $(SCRIPT_DIR)/phase0/build_spec.py $@ $(SPEC_DIR)/core/0_beacon-chain.md $(SPEC_DIR)/core/0_fork-choice.md


CURRENT_DIR = ${CURDIR}
Expand Down
21 changes: 14 additions & 7 deletions scripts/phase0/build_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import function_puller


def build_phase0_spec(sourcefile, outfile):
def build_phase0_spec(outfile, sourcefiles):
code_lines = []
code_lines.append("""

Expand All @@ -13,6 +13,12 @@ def build_phase0_spec(sourcefile, outfile):
NewType,
Tuple,
)

from dataclasses import (
dataclass,
field,
)

from eth2spec.utils.minimal_ssz import (
SSZType,
hash_tree_root,
Expand All @@ -39,10 +45,11 @@ def build_phase0_spec(sourcefile, outfile):
BLSSignature = NewType('BLSSignature', bytes) # bytes96
Store = None
""")

code_lines += function_puller.get_spec(sourcefile)
for index, sourcefile in enumerate(sourcefiles):
code_lines += function_puller.get_spec(sourcefile, index)

code_lines.append("""

# Monkey patch validator compute committee code
_compute_committee = compute_committee
committee_cache = {}
Expand Down Expand Up @@ -91,7 +98,7 @@ def apply_constants_preset(preset: Dict[str, Any]):


if __name__ == '__main__':
if len(sys.argv) < 3:
print("Usage: <source phase0> <output phase0 pyspec>")
build_phase0_spec(sys.argv[1], sys.argv[2])

if len(sys.argv) <= 3:
print("Usage: <output phase0 pyspec> <source 0_beacon-chain> <source 0_fork-choice>")
files = sys.argv[2:]
build_phase0_spec(sys.argv[1], files)
47 changes: 26 additions & 21 deletions scripts/phase0/function_puller.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import sys
from typing import List


def get_spec(file_name: str) -> List[str]:
def get_spec(file_name: str, file_index: int=0) -> List[str]:
code_lines = []
pulling_from = None
current_name = None
current_typedef = None
type_defs = []
for linenum, line in enumerate(open(sys.argv[1]).readlines()):
for linenum, line in enumerate(open(file_name).readlines()):
line = line.rstrip()
if pulling_from is None and len(line) > 0 and line[0] == '#' and line[-1] == '`':
current_name = line[line[:-1].rfind('`') + 1: -1]
Expand All @@ -32,7 +31,7 @@ def get_spec(file_name: str) -> List[str]:
current_typedef = ['global_vars["%s"] = SSZType({' % current_name]
elif pulling_from is not None:
# Add some whitespace between functions
if line[:3] == 'def':
if line[:3] == 'def' or line[:10] == '@dataclass':
code_lines.append('')
code_lines.append('')
code_lines.append(line)
Expand All @@ -55,21 +54,27 @@ def get_spec(file_name: str) -> List[str]:
if eligible:
code_lines.append(row[0] + ' = ' + (row[1].replace('**TBD**', '0x1234567890123456789012345678901234567890')))
# Build type-def re-initialization
code_lines.append('\n')
code_lines.append('def init_SSZ_types():')
code_lines.append(' global_vars = globals()')
for ssz_type_name, ssz_type in type_defs:
code_lines.append('')
for type_line in ssz_type:
if len(type_line) > 0:
code_lines.append(' ' + type_line)
code_lines.append('\n')
code_lines.append('ssz_types = [\n')
for (ssz_type_name, _) in type_defs:
code_lines.append(f' {ssz_type_name},\n')
code_lines.append(']')
code_lines.append('\n')
code_lines.append('def get_ssz_type_by_name(name: str) -> SSZType:')
code_lines.append(' return globals()[name]')
code_lines.append('')
if file_index == 0:
code_lines.append('\n')
code_lines.append('def init_SSZ_types():')
code_lines.append(' global_vars = globals()')
for ssz_type_name, ssz_type in type_defs:
code_lines.append('')
for type_line in ssz_type:
if len(type_line) > 0:
code_lines.append(' ' + type_line)
code_lines.append('\n')
code_lines.append('ssz_types = [\n')
for (ssz_type_name, _) in type_defs:
code_lines.append(f' {ssz_type_name},\n')
code_lines.append(']')
code_lines.append('\n')
code_lines.append('def get_ssz_type_by_name(name: str) -> SSZType:')
code_lines.append(' return globals()[name]')
code_lines.append('\n')

return code_lines

if __name__ == '__main__':
for thing in get_spec(sys.argv[1]):
print(thing)
188 changes: 124 additions & 64 deletions specs/core/0_fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Copy link
Contributor

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 inside get_ancestor, it can't access the given block from store.

As a beacon node, I support it needs to store the block before these verifications?

# 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)
Copy link
Contributor

@hwwhww hwwhww May 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhhh a mutability issue here! 💣

  1. state_transition returns a state, but note that there is side effect - the given state input would be changed anyway. (Avoid side effect in state transition with mutable object #1059)
  2. Since store.states is a dict, the updated pre_state would mess up store.states.

A quick fix for this PR might still be adding the scary deepcopy inside state_transition that I know people really don't want to have in the spec. :'(

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think it's impossible of that both state.current_justified_epoch > slot_to_epoch(store.blocks[store.justified_root].slot) and state.previous_justified_epoch > slot_to_epoch(store.blocks[store.justified_root].slot) happen?
  2. IMHO it's better to avoid using the possible updated store.justified_root in the second condition check (if state.previous_justified_epoch > ...), it looks confusing.

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)
```
4 changes: 4 additions & 0 deletions test_libs/pyspec/eth2spec/utils/minimal_ssz.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
from typing import Any

from .hash_function import hash
Expand Down Expand Up @@ -34,6 +35,9 @@ def serialize(self):
def hash_tree_root(self):
return hash_tree_root(self, self.__class__)

def copy(self):
return copy.deepcopy(self)

SSZObject.fields = fields
return SSZObject

Expand Down
1 change: 1 addition & 0 deletions test_libs/pyspec/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading