From 405e2185981a689ac98871ed3e430435ddb26a04 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 5 Nov 2019 08:55:34 -0700 Subject: [PATCH] add initial fork choice bounce prevention and tests --- specs/core/0_fork-choice.md | 50 ++++++++++++- .../eth2spec/test/fork_choice/test_on_tick.py | 72 +++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 test_libs/pyspec/eth2spec/test/fork_choice/test_on_tick.py diff --git a/specs/core/0_fork-choice.md b/specs/core/0_fork-choice.md index b909ce732..722695f23 100644 --- a/specs/core/0_fork-choice.md +++ b/specs/core/0_fork-choice.md @@ -43,6 +43,12 @@ The head block root associated with a `store` is defined as `get_head(store)`. A 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`. 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). +### Configuration + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `9` | slots | 108 seconds | + ### Helpers #### `LatestMessage` @@ -60,8 +66,10 @@ class LatestMessage(object): @dataclass class Store(object): time: uint64 + genesis_time: uint64 justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint + queued_justified_checkpoints: List[Checkpoint, 2**40] = field(default_factory=list) blocks: Dict[Hash, BeaconBlock] = field(default_factory=dict) block_states: Dict[Hash, BeaconState] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) @@ -78,6 +86,7 @@ def get_genesis_store(genesis_state: BeaconState) -> Store: finalized_checkpoint = Checkpoint(epoch=GENESIS_EPOCH, root=root) return Store( time=genesis_state.genesis_time, + genesis_time=genesis_state.genesis_time, justified_checkpoint=justified_checkpoint, finalized_checkpoint=finalized_checkpoint, blocks={root: genesis_block}, @@ -86,6 +95,11 @@ def get_genesis_store(genesis_state: BeaconState) -> Store: ) ``` +```python +def get_current_slot(store: Store) -> Slot: + return Slot((store.time - store.genesis_time) // SECONDS_PER_SLOT) +``` + #### `get_ancestor` ```python @@ -130,13 +144,44 @@ def get_head(store: Store) -> Hash: head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root)) ``` +#### `should_update_justified_checkpoint` + +```python +def should_update_justified_checkpoint(store: Store, justified_checkpoint: Checkpoint) -> bool: + current_epoch = compute_epoch_at_slot(get_current_slot(store)) + + if get_current_slot(store) - compute_start_slot_at_epoch(current_epoch) < SAFE_SLOTS_TO_UPDATE_JUSTIFIED: + return True + + justified_block = store.blocks[justified_checkpoint.root] + if justified_block.slot <= compute_start_slot_at_epoch(store.justified_checkpoint.epoch): + return False + if not get_ancestor(store, justified_checkpoint.root, store.blocks[justified_checkpoint.root].slot): + return False + + return True +``` + ### Handlers #### `on_tick` ```python def on_tick(store: Store, time: uint64) -> None: + previous_slot = get_current_slot(store) + + # update store time store.time = time + + current_slot = get_current_slot(store) + # not a new epoch, return + if not (current_slot > previous_slot and current_slot % SLOTS_PER_EPOCH == 0): + return + # if new epoch and there are queued_justified_checkpoints, update if any is better than the best in store + if any(store.queued_justified_checkpoints): + best_justified_checkpoint = max(store.queued_justified_checkpoints, key=lambda checkpoint: checkpoint.epoch) + if best_justified_checkpoint.epoch > store.justified_checkpoint.epoch: + store.justified_checkpoint = best_justified_checkpoint ``` #### `on_block` @@ -164,7 +209,10 @@ def on_block(store: Store, block: BeaconBlock) -> None: # Update justified checkpoint if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: - store.justified_checkpoint = state.current_justified_checkpoint + if should_update_justified_checkpoint(store, state.current_justified_checkpoint): + store.justified_checkpoint = state.current_justified_checkpoint + else: + store.queued_justified_checkpoints.append(state.current_justified_checkpoint) # Update finalized checkpoint if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch: diff --git a/test_libs/pyspec/eth2spec/test/fork_choice/test_on_tick.py b/test_libs/pyspec/eth2spec/test/fork_choice/test_on_tick.py new file mode 100644 index 000000000..cd0d20abb --- /dev/null +++ b/test_libs/pyspec/eth2spec/test/fork_choice/test_on_tick.py @@ -0,0 +1,72 @@ +from eth2spec.test.context import with_all_phases, spec_state_test + + +def run_on_tick(spec, store, time, new_justified_checkpoint=None): + previous_justified_checkpoint = store.justified_checkpoint + + spec.on_tick(store, time) + + assert store.time == time + + if new_justified_checkpoint: + assert store.justified_checkpoint == new_justified_checkpoint + assert store.justified_checkpoint.epoch > previous_justified_checkpoint.epoch + else: + assert store.justified_checkpoint == previous_justified_checkpoint + + +@with_all_phases +@spec_state_test +def test_basic(spec, state): + store = spec.get_genesis_store(state) + run_on_tick(spec, store, store.time + 1) + + +@with_all_phases +@spec_state_test +def test_update_justified_single(spec, state): + store = spec.get_genesis_store(state) + seconds_per_epoch = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH + + new_justified = spec.Checkpoint( + epoch=store.justified_checkpoint.epoch + 1, + root=b'\x55' * 32, + ) + + store.queued_justified_checkpoints.append(new_justified) + + run_on_tick(spec, store, store.time + seconds_per_epoch, new_justified) + + +@with_all_phases +@spec_state_test +def test_update_justified_multiple(spec, state): + store = spec.get_genesis_store(state) + seconds_per_epoch = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH + + new_justified = None # remember checkpoint with latest epoch + for i in range(3): + new_justified = spec.Checkpoint( + epoch=store.justified_checkpoint.epoch + i + 1, + root=i.to_bytes(1, byteorder='big') * 32, + ) + store.queued_justified_checkpoints.append(new_justified) + + run_on_tick(spec, store, store.time + seconds_per_epoch, new_justified) + + +@with_all_phases +@spec_state_test +def test_no_update_(spec, state): + store = spec.get_genesis_store(state) + seconds_per_epoch = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH + + new_justified = spec.Checkpoint( + epoch=store.justified_checkpoint.epoch + 1, + root=b'\x55' * 32, + ) + + store.queued_justified_checkpoints.append(new_justified) + + run_on_tick(spec, store, store.time + seconds_per_epoch, new_justified) +