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

Executable fork choice #1185

Merged
merged 33 commits into from
Jun 21, 2019
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
11edda6
Adds fork-choice to spec builder
CarlBeek Jun 15, 2019
061ecf7
Adds fork-choice tests
CarlBeek Jun 15, 2019
5d10cd6
SSZ (partially) handles Dicts
CarlBeek Jun 16, 2019
4deb311
Debugging 1st test
CarlBeek Jun 16, 2019
f421850
Fixes tests! Thanks @hwwhww!
CarlBeek Jun 16, 2019
9f2d06b
Somehow I had indented with 5 spaces everywhere.
CarlBeek Jun 16, 2019
20d8156
Merge branch 'dev' into executable_fork_choice
CarlBeek Jun 16, 2019
2ea6ced
Moves fork-choice objects away from SSZ
CarlBeek Jun 17, 2019
bb36660
Kick the CI cache
CarlBeek Jun 17, 2019
c31a426
Kick the other CI cache
CarlBeek Jun 17, 2019
31a4700
Merge conflicts on `dev`
CarlBeek Jun 18, 2019
d5d35b1
Trail upgrade CI to python 3.7
CarlBeek Jun 18, 2019
28a3e54
Actually resolve some merge conflicts
CarlBeek Jun 18, 2019
eb9fc7b
Remove circle ci repo caching
CarlBeek Jun 18, 2019
dfdd283
Python 3.7 -> 3.6 again
CarlBeek Jun 18, 2019
686273e
Maybe venv cache name collision
CarlBeek Jun 18, 2019
3bcddf5
Removes defaults from Store to try get CI to pass
CarlBeek Jun 18, 2019
d804cb3
Removes defaults from Store to try get CI to pass
CarlBeek Jun 18, 2019
7a71919
Test removing all the Bytes objects
CarlBeek Jun 18, 2019
dc01218
Add `dataclasses==0.6` to setup.py
hwwhww Jun 18, 2019
04c7c43
kick deposit contract cache
hwwhww Jun 18, 2019
61a5141
Unbreak things again
CarlBeek Jun 18, 2019
3418c35
Merge dev and resolve conflicts (please let the CI tests pass with th…
CarlBeek Jun 19, 2019
241fe34
Workaround for python3.6
CarlBeek Jun 19, 2019
e0d814d
Merge branch 'dev' into executable_fork_choice
CarlBeek Jun 19, 2019
d73aa31
Cleanups
JustinDrake Jun 20, 2019
5f8edd6
Genesis block store uses genesis time
CarlBeek Jun 20, 2019
c26fffc
Moves copy into SSZ container
CarlBeek Jun 20, 2019
e88a96c
Apply suggestions from @drjtwo's code review
CarlBeek Jun 21, 2019
f90469e
Move block timing assertion 1st
CarlBeek Jun 21, 2019
0e59c66
Stop yielding from fork-choie tests
CarlBeek Jun 21, 2019
b46e047
Minor simplification from #1198
JustinDrake Jun 21, 2019
acbccbc
minor typo
djrtwo Jun 21, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,26 @@ commands:
description: "Restore the cache with pyspec keys"
steps:
- restore_cached_venv:
venv_name: v2-pyspec
venv_name: v3-pyspec-bump2
reqs_checksum: cache-{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}
save_pyspec_cached_venv:
description: Save a venv into a cache with pyspec keys"
steps:
- save_cached_venv:
venv_name: v2-pyspec
venv_name: v3-pyspec-bump2
reqs_checksum: cache-{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "test_libs/pyspec/requirements-testing.txt" }}
venv_path: ./test_libs/pyspec/venv
restore_deposit_contract_cached_venv:
description: "Restore the cache with deposit_contract keys"
steps:
- restore_cached_venv:
venv_name: v4-deposit-contract
venv_name: v6-deposit-contract
reqs_checksum: cache-{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "deposit_contract/requirements-testing.txt" }}
save_deposit_contract_cached_venv:
description: Save a venv into a cache with deposit_contract keys"
steps:
- save_cached_venv:
venv_name: v4-deposit-contract
venv_name: v6-deposit-contract
reqs_checksum: cache-{{ checksum "test_libs/pyspec/requirements.txt" }}-{{ checksum "deposit_contract/requirements-testing.txt" }}
venv_path: ./deposit_contract/venv
jobs:
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ test_deposit_contract:
pyspec: $(PY_SPEC_ALL_TARGETS)

$(PY_SPEC_PHASE_0_TARGETS): $(PY_SPEC_PHASE_0_DEPS)
python3 $(SCRIPT_DIR)/build_spec.py -p0 $(SPEC_DIR)/core/0_beacon-chain.md $@
python3 $(SCRIPT_DIR)/build_spec.py -p0 $(SPEC_DIR)/core/0_beacon-chain.md $(SPEC_DIR)/core/0_fork-choice.md $@

$(PY_SPEC_DIR)/eth2spec/phase1/spec.py: $(PY_SPEC_PHASE_1_DEPS)
python3 $(SCRIPT_DIR)/build_spec.py -p1 $(SPEC_DIR)/core/0_beacon-chain.md $(SPEC_DIR)/core/1_custody-game.md $(SPEC_DIR)/core/1_shard-data-chains.md $@
python3 $(SCRIPT_DIR)/build_spec.py -p1 $(SPEC_DIR)/core/0_beacon-chain.md $(SPEC_DIR)/core/1_custody-game.md $(SPEC_DIR)/core/1_shard-data-chains.md $(SPEC_DIR)/core/0_fork-choice.md $@

CURRENT_DIR = ${CURDIR}

Expand Down
40 changes: 30 additions & 10 deletions scripts/build_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
Tuple,
)

from dataclasses import (
dataclass,
field,
)

from copy import deepcopy

from eth2spec.utils.ssz.ssz_impl import (
hash_tree_root,
signing_root,
Expand Down Expand Up @@ -48,6 +55,13 @@
Tuple,
)

from dataclasses import (
dataclass,
field,
)

from copy import deepcopy

from eth2spec.utils.ssz.ssz_impl import (
hash_tree_root,
signing_root,
Expand Down Expand Up @@ -223,9 +237,11 @@ def combine_spec_objects(spec0: SpecObject, spec1: SpecObject) -> SpecObject:
return functions, custom_types, constants, ssz_objects, inserts


def build_phase0_spec(sourcefile: str, outfile: str=None) -> Optional[str]:
functions, custom_types, constants, ssz_objects, inserts = get_spec(sourcefile)
spec = objects_to_spec(functions, custom_types, constants, ssz_objects, inserts, PHASE0_IMPORTS)
def build_phase0_spec(phase0_sourcefile: str, fork_choice_sourcefile: str, outfile: str=None) -> Optional[str]:
phase0_spec = get_spec(phase0_sourcefile)
fork_choice_spec = get_spec(fork_choice_sourcefile)
spec_objects = combine_spec_objects(phase0_spec, fork_choice_spec)
spec = objects_to_spec(*spec_objects, PHASE0_IMPORTS)
if outfile is not None:
with open(outfile, 'w') as out:
out.write(spec)
Expand All @@ -235,12 +251,14 @@ def build_phase0_spec(sourcefile: str, outfile: str=None) -> Optional[str]:
def build_phase1_spec(phase0_sourcefile: str,
phase1_custody_sourcefile: str,
phase1_shard_sourcefile: str,
fork_choice_sourcefile: str,
outfile: str=None) -> Optional[str]:
phase0_spec = get_spec(phase0_sourcefile)
phase1_custody = get_spec(phase1_custody_sourcefile)
phase1_shard_data = get_spec(phase1_shard_sourcefile)
fork_choice_spec = get_spec(fork_choice_sourcefile)
spec_objects = phase0_spec
for value in [phase1_custody, phase1_shard_data]:
for value in [phase1_custody, phase1_shard_data, fork_choice_spec]:
spec_objects = combine_spec_objects(spec_objects, value)
spec = objects_to_spec(*spec_objects, PHASE1_IMPORTS)
if outfile is not None:
Expand All @@ -254,28 +272,30 @@ def build_phase1_spec(phase0_sourcefile: str,
Build the specs from the md docs.
If building phase 0:
1st argument is input spec.md
2nd argument is output spec.py
2nd argument is input fork_choice.md
3rd argument is output spec.py

If building phase 1:
1st argument is input spec_phase0.md
2nd argument is input spec_phase1_custody.md
3rd argument is input spec_phase1_shard_data.md
4th argument is output spec.py
4th argument is input fork_choice.md
5th argument is output spec.py
'''
parser = ArgumentParser(description=description)
parser.add_argument("-p", "--phase", dest="phase", type=int, default=0, help="Build for phase #")
parser.add_argument(dest="files", help="Input and output files", nargs="+")

args = parser.parse_args()
if args.phase == 0:
if len(args.files) == 2:
if len(args.files) == 3:
build_phase0_spec(*args.files)
else:
print(" Phase 0 requires an output as well as an input file.")
print(" Phase 0 requires an output as well as spec and forkchoice files.")
elif args.phase == 1:
if len(args.files) == 4:
if len(args.files) == 5:
build_phase1_spec(*args.files)
else:
print(" Phase 1 requires an output as well as 3 input files (phase0.md and phase1.md, phase1.md)")
print(" Phase 1 requires an output as well as 4 input files (phase0.md and phase1.md, phase1.md, fork_choice.md)")
else:
print("Invalid phase: {0}".format(args.phase))
1 change: 1 addition & 0 deletions scripts/function_puller.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def get_spec(file_name: str) -> SpecObject:
inserts = {}
function_matcher = re.compile(FUNCTION_REGEX)
inserts_matcher = re.compile(BEGIN_INSERT_REGEX)
is_ssz = False
custom_types = {}
for linenum, line in enumerate(open(file_name).readlines()):
line = line.rstrip()
Expand Down
188 changes: 123 additions & 65 deletions specs/core/0_fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,26 @@
- [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)
- [Configuration](#configuration)
- [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)
- [Helpers](#helpers)
- [`Target`](#target)
- [`Store`](#store)
- [`get_genesis_store`](#get_genesis_store)
- [`get_ancestor`](#get_ancestor)
- [`get_latest_attesting_balance`](#get_latest_attesting_balance)
- [`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).

## Configuration

Expand All @@ -34,76 +37,131 @@ 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.
CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
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`.

CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
### Helpers

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`

* The parent block with root `block.parent_root` has been processed and accepted.
* An Ethereum 1.0 block pointed to by the `state.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`.
```python
@dataclass
class Target(object):
epoch: Epoch
root: Hash
```

*Note*: Leap seconds mean that slots will occasionally last `SECONDS_PER_SLOT + 1` or `SECONDS_PER_SLOT - 1` seconds, possibly several times a year.
#### `Store`

*Note*: Nodes needs to have a clock that is roughly (i.e. within `SECONDS_PER_SLOT` seconds) synchronized with the other nodes.
```python
@dataclass
class Store(object):
blocks: Dict[Hash, BeaconBlock] = field(default_factory=dict)
states: Dict[Hash, BeaconState] = field(default_factory=dict)
time: int = 0
latest_targets: Dict[ValidatorIndex, Target] = field(default_factory=dict)
justified_root: Hash = ZERO_HASH
finalized_root: Hash = ZERO_HASH
```

### Beacon chain fork choice rule
#### `get_genesis_store`

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_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}, justified_root=root, finalized_root=root)
CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
```

* 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_ancestor`

```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_ancestor(store: Store, root: Hash, slot: Slot) -> Hash:
block = store.blocks[root]
assert block.slot >= slot
return root if block.slot == slot else get_ancestor(store, block.parent_root, slot)
```

* 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_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_latest_attesting_balance`

```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.validators
active_validator_indices = get_active_validator_indices(validators, slot_to_epoch(start_state.slot))
attestation_targets = [(i, get_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.validators[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_latest_attesting_balance(store: Store, root: Hash) -> Gwei:
state = store.states[store.justified_root]
active_indices = get_active_validator_indices(state.validator_registry, slot_to_epoch(state.slot))
return Gwei(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
))
```

#### `get_head`

```python
def get_head(store: Store) -> Hash:
# 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 latest attesting balance with ties broken lexicographically
head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root))
```

## Implementation notes
### Handlers

#### `on_tick`

```python
def on_tick(store: Store, time: int) -> None:
store.time = time
```

### Justification and finality at genesis
#### `on_block`

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.
```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 = deepcopy(store.states[block.parent_root])
CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
assert store.time >= pre_state.genesis_time + block.slot * SECONDS_PER_SLOT
CarlBeek marked this conversation as resolved.
Show resolved Hide resolved
# Check the block is valid and compute the post-state
state = state_transition(pre_state, block)
# Add new state to the store
store.states[signing_root(block)] = state
# Update justified block root
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
# Update finalized block root
if state.finalized_epoch > slot_to_epoch(store.blocks[store.finalized_root].slot):
store.finalized_root = state.finalized_root
```

#### `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)
```
1 change: 0 additions & 1 deletion specs/core/1_shard-data-chains.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ This document describes the shard data layer and the shard fork choice rule in P
| - | - | :-: | :-: |
| `CROSSLINK_LOOKBACK` | `2**0` (= 1) | epochs | 6.2 minutes |
| `PERSISTENT_COMMITTEE_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days |
| `SECONDS_PER_SLOT` | `2**1 * 3**1` (= 6) | 6 seconds |

### Signature domains

Expand Down
Loading