diff --git a/Makefile b/Makefile index a6b379b719..db091c7b57 100644 --- a/Makefile +++ b/Makefile @@ -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} diff --git a/scripts/phase0/build_spec.py b/scripts/phase0/build_spec.py index 14226cbd40..8bec46a1cd 100644 --- a/scripts/phase0/build_spec.py +++ b/scripts/phase0/build_spec.py @@ -2,7 +2,7 @@ import function_puller -def build_phase0_spec(sourcefile, outfile): +def build_phase0_spec(outfile, sourcefiles): code_lines = [] code_lines.append(""" @@ -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, @@ -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 = {} @@ -91,7 +98,7 @@ def apply_constants_preset(preset: Dict[str, Any]): if __name__ == '__main__': - if len(sys.argv) < 3: - print("Usage: ") - build_phase0_spec(sys.argv[1], sys.argv[2]) - + if len(sys.argv) <= 3: + print("Usage: ") + files = sys.argv[2:] + build_phase0_spec(sys.argv[1], files) diff --git a/scripts/phase0/function_puller.py b/scripts/phase0/function_puller.py index 750f195904..3da892a688 100644 --- a/scripts/phase0/function_puller.py +++ b/scripts/phase0/function_puller.py @@ -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] @@ -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) @@ -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) diff --git a/specs/core/0_fork-choice.md b/specs/core/0_fork-choice.md index 91c3e27ee4..79301171d5 100644 --- a/specs/core/0_fork-choice.md +++ b/specs/core/0_fork-choice.md @@ -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) ## 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) + # 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 +``` -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) +``` diff --git a/test_libs/pyspec/eth2spec/utils/minimal_ssz.py b/test_libs/pyspec/eth2spec/utils/minimal_ssz.py index 9cc2baebb3..ca9922ccf0 100644 --- a/test_libs/pyspec/eth2spec/utils/minimal_ssz.py +++ b/test_libs/pyspec/eth2spec/utils/minimal_ssz.py @@ -1,3 +1,4 @@ +import copy from typing import Any from .hash_function import hash @@ -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 diff --git a/test_libs/pyspec/requirements.txt b/test_libs/pyspec/requirements.txt index 78d41708dc..8d49ec7171 100644 --- a/test_libs/pyspec/requirements.txt +++ b/test_libs/pyspec/requirements.txt @@ -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 diff --git a/test_libs/pyspec/tests/test_fork_choice.py b/test_libs/pyspec/tests/test_fork_choice.py new file mode 100644 index 0000000000..8dced97f17 --- /dev/null +++ b/test_libs/pyspec/tests/test_fork_choice.py @@ -0,0 +1,50 @@ + +import eth2spec.phase0.spec as spec +from eth2spec.utils.minimal_ssz import ( + signing_root, +) + +from tests.helpers import ( + build_empty_block_for_next_slot, + get_valid_attestation, + next_slot, +) + + +def test_basic(state): + # Initialization + store = spec.get_genesis_store(state) + time = 100 + spec.on_tick(store, time) + assert store.time == time + + # On receiving a block of `GENESIS_SLOT + 1` slot + block = build_empty_block_for_next_slot(state) + spec.on_block(store, block) + assert store.blocks[signing_root(block)] == block + + # On receiving a block of next epoch + store.time = time + spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH + block = build_empty_block_for_next_slot(state) + block.slot += spec.SLOTS_PER_EPOCH + + spec.on_block(store, block) + assert store.blocks[signing_root(block)] == block + + # TODO: add tests for justified_root and finalized_root + + +def test_on_attestation(state): + store = spec.get_genesis_store(state) + time = 100 + spec.on_tick(store, time) + + next_slot(state) + + attestation = get_valid_attestation(state, slot=1) + indexed_attestation = spec.convert_to_indexed(state, attestation) + spec.on_attestation(store, attestation) + assert ( + store.latest_targets[indexed_attestation.custody_bit_0_indices[0]] == + spec.Target(attestation.data.target_epoch, attestation.data.target_root) + )