From 2d161b4244a1318546130ce57f5b87b613951908 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Fri, 19 Nov 2021 12:35:59 -0800 Subject: [PATCH] Add proposer score boosting & related tests --- specs/merge/fork-choice.md | 8 ++ specs/phase0/fork-choice.md | 30 +++++++- .../test/phase0/fork_choice/test_get_head.py | 76 ++++++++++++++++++- .../test/phase0/fork_choice/test_on_block.py | 26 +++++++ 4 files changed, 136 insertions(+), 4 deletions(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 8c5bc1312..f62c8e014 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -175,6 +175,14 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Add new state for this block to the store store.block_states[hash_tree_root(block)] = state + # Add proposer score boost if the block is timely + if (get_current_slot(store) == block.slot and + store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT): + store.proposer_score_boost = LatestMessage( + root=hash_tree_root(block), + epoch=compute_epoch_at_slot(block.slot) + ) + # Update justified checkpoint if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: if state.current_justified_checkpoint.epoch > store.best_justified_checkpoint.epoch: diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 6854a8487..7310d6851 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -61,6 +61,7 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass | Name | Value | Unit | Duration | | - | - | :-: | :-: | | `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds | +| `ATTESTATION_OFFSET_QUOTIENT` | `3` | - | - | ### Helpers @@ -83,6 +84,7 @@ class Store(object): justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint best_justified_checkpoint: Checkpoint + proposer_score_boost: LatestMessage blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) @@ -103,12 +105,14 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - anchor_epoch = get_current_epoch(anchor_state) justified_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root) finalized_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root) + proposer_score_boost = LatestMessage(root=Root(), epoch=Epoch(0)) return Store( time=uint64(anchor_state.genesis_time + SECONDS_PER_SLOT * anchor_state.slot), genesis_time=anchor_state.genesis_time, justified_checkpoint=justified_checkpoint, finalized_checkpoint=finalized_checkpoint, best_justified_checkpoint=justified_checkpoint, + proposer_score_boost=proposer_score_boost, blocks={anchor_root: copy(anchor_block)}, block_states={anchor_root: copy(anchor_state)}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, @@ -156,11 +160,23 @@ def get_ancestor(store: Store, root: Root, slot: Slot) -> Root: def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: state = store.checkpoint_states[store.justified_checkpoint] active_indices = get_active_validator_indices(state, get_current_epoch(state)) - return Gwei(sum( + attestation_score = Gwei(sum( state.validators[i].effective_balance for i in active_indices if (i in store.latest_messages and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) + proposer_score = Gwei(0) + if store.proposer_score_boost.root != Root(): + block_slot = store.blocks[root].slot + if get_ancestor(store, root, block_slot) == store.proposer_score_boost.root: + num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) + avg_balance = get_total_active_balance(state) // num_validators + block_epoch = compute_epoch_at_slot(block_slot) + committee_size = get_committee_count_per_slot(state, block_epoch) * TARGET_COMMITTEE_SIZE + committee_weight = committee_size * avg_balance + proposer_score = committee_weight // 4 + return attestation_score + proposer_score + ``` #### `filter_block_tree` @@ -339,6 +355,10 @@ def on_tick(store: Store, time: uint64) -> None: store.time = time current_slot = get_current_slot(store) + # Reset store.proposer_score_boost if this is a new slot + if store.proposer_score_boost.root != Root(): + if current_slot != store.blocks[store.proposer_score_boost.root].slot: + store.proposer_score_boost = LatestMessage(root=Root(), epoch=Epoch(0)) # Not a new epoch, return if not (current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0): return @@ -377,6 +397,14 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Add new state for this block to the store store.block_states[hash_tree_root(block)] = state + # Add proposer score boost if the block is timely + if (get_current_slot(store) == block.slot and + store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT): + store.proposer_score_boost = LatestMessage( + root=hash_tree_root(block), + epoch=compute_epoch_at_slot(block.slot) + ) + # Update justified checkpoint if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: if state.current_justified_checkpoint.epoch > store.best_justified_checkpoint.epoch: diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py index 12b261e4e..bb9cbee2e 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py @@ -1,3 +1,4 @@ +import random from eth_utils import encode_hex from eth2spec.test.context import ( @@ -19,6 +20,7 @@ from eth2spec.test.helpers.fork_choice import ( add_block, ) from eth2spec.test.helpers.state import ( + next_slots, next_epoch, state_transition_and_sign_block, ) @@ -103,17 +105,21 @@ def test_split_tie_breaker_no_attestations(spec, state): } }) - # block at slot 1 + # Create block at slot 1 block_1_state = genesis_state.copy() block_1 = build_empty_block_for_next_slot(spec, block_1_state) signed_block_1 = state_transition_and_sign_block(spec, block_1_state, block_1) - yield from tick_and_add_block(spec, store, signed_block_1, test_steps) - # additional block at slot 1 + # Create additional block at slot 1 block_2_state = genesis_state.copy() block_2 = build_empty_block_for_next_slot(spec, block_2_state) block_2.body.graffiti = b'\x42' * 32 signed_block_2 = state_transition_and_sign_block(spec, block_2_state, block_2) + + # Tick time past slot 1 so proposer score boost does not apply + spec.on_tick(store, store.genesis_time + (block_2.slot + 1) * spec.config.SECONDS_PER_SLOT) + + yield from tick_and_add_block(spec, store, signed_block_1, test_steps) yield from tick_and_add_block(spec, store, signed_block_2, test_steps) highest_root = max(spec.hash_tree_root(block_1), spec.hash_tree_root(block_2)) @@ -261,3 +267,67 @@ def test_filtered_block_tree(spec, state): }) yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_proposer_score_boost_basic(spec, state): + test_steps = [] + genesis_state = state.copy() + + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + anchor_root = get_anchor_root(spec, state) + assert spec.get_head(store) == anchor_root + test_steps.append({ + 'checks': { + 'head': get_formatted_head_output(spec, store), + } + }) + + # Build block that serves as head ONLY on timely arrival, and ONLY in that slot + state_1 = genesis_state.copy() + next_slots(spec, state_1, 3) + block_1 = build_empty_block_for_next_slot(spec, state_1) + signed_block_1 = state_transition_and_sign_block(spec, state_1, block_1) + + # Build block that serves as current head, and remains the head after block_1.slot + state_2 = genesis_state.copy() + next_slots(spec, state_2, 2) + block_2 = build_empty_block_for_next_slot(spec, state_2) + block_2.body.graffiti = spec.Bytes32(hex(random.getrandbits(8 * 32))[2:].zfill(64)) + signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) + while spec.hash_tree_root(block_1) > spec.hash_tree_root(block_2): + block_2.body.graffiti = spec.Bytes32(hex(random.getrandbits(8 * 32))[2:].zfill(64)) + signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) + assert spec.hash_tree_root(block_1) < spec.hash_tree_root(block_2) + + # Tick to block_1 slot time + spec.on_tick(store, store.genesis_time + block_1.slot * spec.config.SECONDS_PER_SLOT) + + # Process block_2 + yield from tick_and_add_block(spec, store, signed_block_2, test_steps) + assert store.proposer_score_boost.root == spec.Root() + assert spec.get_head(store) == spec.hash_tree_root(block_2) + + # Process block_1 on timely arrival + # The head should temporarily change to block_1 + yield from tick_and_add_block(spec, store, signed_block_1, test_steps) + assert store.proposer_score_boost == spec.LatestMessage(root=spec.hash_tree_root(block_1), + epoch=spec.compute_epoch_at_slot(block_1.slot)) + assert spec.get_head(store) == spec.hash_tree_root(block_1) + + # After block_1.slot, the head should revert to block_2 + spec.on_tick(store, store.genesis_time + (block_1.slot + 1) * spec.config.SECONDS_PER_SLOT) + assert store.proposer_score_boost.root == spec.Root() + assert spec.get_head(store) == spec.hash_tree_root(block_2) + + test_steps.append({ + 'checks': { + 'head': get_formatted_head_output(spec, store), + } + }) + + yield 'steps', test_steps diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py index 7194c6a20..6cf04559b 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py @@ -703,3 +703,29 @@ def test_new_finalized_slot_is_justified_checkpoint_ancestor(spec, state): assert store.justified_checkpoint == another_state.current_justified_checkpoint yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_proposer_score_boost_same_slot_untimely_block(spec, state): + test_steps = [] + genesis_state = state.copy() + + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + + # Build block that serves as head ONLY on timely arrival, and ONLY in that slot + state = genesis_state.copy() + next_slots(spec, state, 3) + block = build_empty_block_for_next_slot(spec, state) + signed_block = state_transition_and_sign_block(spec, state, block) + + # Process block on untimely arrival in the same slot + spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + + spec.config.SECONDS_PER_SLOT // spec.ATTESTATION_OFFSET_QUOTIENT) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert store.proposer_score_boost.root == spec.Root() + + yield 'steps', test_steps