From 2d161b4244a1318546130ce57f5b87b613951908 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Fri, 19 Nov 2021 12:35:59 -0800 Subject: [PATCH 01/20] 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 From 281c1b2d1ab333710f07e80bcd07a172db97d07c Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Fri, 19 Nov 2021 12:47:33 -0800 Subject: [PATCH 02/20] Update validator guide with ATTESTATION_OFFSET_QUOTIENT --- specs/phase0/validator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index cc5b7aeb5..b25a9a8b3 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -446,7 +446,7 @@ def get_block_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> A validator is expected to create, sign, and broadcast an attestation during each epoch. The `committee`, assigned `index`, and assigned `slot` for which the validator performs this role during an epoch are defined by `get_committee_assignment(state, epoch, validator_index)`. -A validator should create and broadcast the `attestation` to the associated attestation subnet when either (a) the validator has received a valid block from the expected block proposer for the assigned `slot` or (b) one-third of the `slot` has transpired (`SECONDS_PER_SLOT / 3` seconds after the start of `slot`) -- whichever comes _first_. +A validator should create and broadcast the `attestation` to the associated attestation subnet when either (a) the validator has received a valid block from the expected block proposer for the assigned `slot` or (b) one-third of the `slot` has transpired (`SECONDS_PER_SLOT / ATTESTATION_OFFSET_QUOTIENT` seconds after the start of `slot`) -- whichever comes _first_. *Note*: Although attestations during `GENESIS_EPOCH` do not count toward FFG finality, these initial attestations do give weight to the fork choice, are rewarded, and should be made. From 47fa6d108a9d7d0a5fbfc127219a34f1cfd86b39 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Fri, 19 Nov 2021 18:27:47 -0800 Subject: [PATCH 03/20] Add parameter for score boost value --- specs/phase0/fork-choice.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 7310d6851..65783d34d 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -8,6 +8,7 @@ - [Introduction](#introduction) - [Fork choice](#fork-choice) - [Preset](#preset) + - [Configuration](#configuration) - [Helpers](#helpers) - [`LatestMessage`](#latestmessage) - [`Store`](#store) @@ -63,6 +64,12 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass | `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds | | `ATTESTATION_OFFSET_QUOTIENT` | `3` | - | - | +### Configuration + +| Name | Value | +| - | - | +| `PROPOSER_SCORE_BOOST_QUOTIENT` | `4` | + ### Helpers #### `LatestMessage` @@ -174,7 +181,7 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: 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 + proposer_score = committee_weight // PROPOSER_SCORE_BOOST_QUOTIENT return attestation_score + proposer_score ``` From 504d82cc1ac751bb39c14323a73bd9ebea92ca5e Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Sat, 20 Nov 2021 09:16:43 -0800 Subject: [PATCH 04/20] Add datatype to new parameters --- specs/phase0/fork-choice.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 65783d34d..e43fca3a5 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -62,13 +62,13 @@ 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` | - | - | +| `ATTESTATION_OFFSET_QUOTIENT` | `uint64(3)` | - | - | ### Configuration | Name | Value | | - | - | -| `PROPOSER_SCORE_BOOST_QUOTIENT` | `4` | +| `PROPOSER_SCORE_BOOST_QUOTIENT` | `uint64(4)` | ### Helpers From b0fb861cf51197df71736db06ebcb3363950791d Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Sat, 20 Nov 2021 09:35:45 -0800 Subject: [PATCH 05/20] Make PROPOSER_SCORE_BOOST a percentage value --- specs/phase0/fork-choice.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index e43fca3a5..5cb87a061 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -68,7 +68,9 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass | Name | Value | | - | - | -| `PROPOSER_SCORE_BOOST_QUOTIENT` | `uint64(4)` | +| `PROPOSER_SCORE_BOOST` | `uint64(25)` | + +- The proposer score boost is worth `PROPOSER_SCORE_BOOST` percentage of the committee's weight, i.e., for slot with committee weight `committee_weight` the boost weight is equal to `(committee_weight * PROPOSER_SCORE_BOOST) // 100`. ### Helpers @@ -181,7 +183,7 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: 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 // PROPOSER_SCORE_BOOST_QUOTIENT + proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 return attestation_score + proposer_score ``` From 3b20e3ea02828c367be7841020e4be4eafb21263 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Mon, 22 Nov 2021 08:38:08 -0800 Subject: [PATCH 06/20] Apply suggestions from code review Co-authored-by: Danny Ryan --- specs/merge/fork-choice.md | 4 ++-- specs/phase0/fork-choice.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index f62c8e014..342ef0f69 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -176,8 +176,8 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: 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): + is_before_attestation_broadcast = store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT + if get_current_slot(store) == block.slot and is_before_attestation_broadcast: store.proposer_score_boost = LatestMessage( root=hash_tree_root(block), epoch=compute_epoch_at_slot(block.slot) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 5cb87a061..e3e18f9b1 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -114,7 +114,7 @@ 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)) + proposer_score_boost = LatestMessage(root=Root(), epoch=Epoch()) return Store( time=uint64(anchor_state.genesis_time + SECONDS_PER_SLOT * anchor_state.slot), genesis_time=anchor_state.genesis_time, From 859bbf435854d14bc397817d25cbb613fc465afe Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Mon, 22 Nov 2021 08:48:40 -0800 Subject: [PATCH 07/20] This reverts commit 4c726cdff39a10c5d096b294fb562cfc99c1f068. --- specs/merge/fork-choice.md | 4 ++-- specs/phase0/fork-choice.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 342ef0f69..f62c8e014 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -176,8 +176,8 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: store.block_states[hash_tree_root(block)] = state # Add proposer score boost if the block is timely - is_before_attestation_broadcast = store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT - if get_current_slot(store) == block.slot and is_before_attestation_broadcast: + 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) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index e3e18f9b1..5cb87a061 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -114,7 +114,7 @@ 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()) + 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, From 88c76abd7fba95ef8352dbf2b256e7eb3687a65b Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Mon, 22 Nov 2021 09:34:32 -0800 Subject: [PATCH 08/20] Apply Danny's code review --- specs/merge/fork-choice.md | 6 ++++-- specs/phase0/fork-choice.md | 13 +++++++------ specs/phase0/validator.md | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index f62c8e014..bf67d2f4f 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -176,8 +176,10 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: 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): + is_before_attestation_broadcast = ( + store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT + ) + if get_current_slot(store) == block.slot and is_before_attestation_broadcast: store.proposer_score_boost = LatestMessage( root=hash_tree_root(block), epoch=compute_epoch_at_slot(block.slot) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 5cb87a061..f302887ec 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -176,12 +176,11 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: )) 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: + block = store.blocks[root] + 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_size = num_validators // SLOTS_PER_EPOCH committee_weight = committee_size * avg_balance proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 return attestation_score + proposer_score @@ -407,8 +406,10 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: 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): + is_before_attestation_broadcast = ( + store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT + ) + if get_current_slot(store) == block.slot and is_before_attestation_broadcast: store.proposer_score_boost = LatestMessage( root=hash_tree_root(block), epoch=compute_epoch_at_slot(block.slot) diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index b25a9a8b3..209cc7e5d 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -446,7 +446,7 @@ def get_block_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> A validator is expected to create, sign, and broadcast an attestation during each epoch. The `committee`, assigned `index`, and assigned `slot` for which the validator performs this role during an epoch are defined by `get_committee_assignment(state, epoch, validator_index)`. -A validator should create and broadcast the `attestation` to the associated attestation subnet when either (a) the validator has received a valid block from the expected block proposer for the assigned `slot` or (b) one-third of the `slot` has transpired (`SECONDS_PER_SLOT / ATTESTATION_OFFSET_QUOTIENT` seconds after the start of `slot`) -- whichever comes _first_. +A validator should create and broadcast the `attestation` to the associated attestation subnet when either (a) the validator has received a valid block from the expected block proposer for the assigned `slot` or (b) `1 / ATTESTATION_OFFSET_QUOTIENT` of the `slot` has transpired (`SECONDS_PER_SLOT / ATTESTATION_OFFSET_QUOTIENT` seconds after the start of `slot`) -- whichever comes _first_. *Note*: Although attestations during `GENESIS_EPOCH` do not count toward FFG finality, these initial attestations do give weight to the fork choice, are rewarded, and should be made. @@ -569,7 +569,7 @@ def get_aggregate_signature(attestations: Sequence[Attestation]) -> BLSSignature #### Broadcast aggregate -If the validator is selected to aggregate (`is_aggregator`), then they broadcast their best aggregate as a `SignedAggregateAndProof` to the global aggregate channel (`beacon_aggregate_and_proof`) two-thirds of the way through the `slot`-that is, `SECONDS_PER_SLOT * 2 / 3` seconds after the start of `slot`. +If the validator is selected to aggregate (`is_aggregator`), then they broadcast their best aggregate as a `SignedAggregateAndProof` to the global aggregate channel (`beacon_aggregate_and_proof`) `2 / ATTESTATION_OFFSET_QUOTIENT` of the way through the `slot`-that is, `SECONDS_PER_SLOT * 2 / ATTESTATION_OFFSET_QUOTIENT` seconds after the start of `slot`. Selection proofs are provided in `AggregateAndProof` to prove to the gossip channel that the validator has been selected as an aggregator. From cebe6ba7e75d0917bc4f6891d49ebda1200b347c Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 22 Nov 2021 11:29:12 -0700 Subject: [PATCH 09/20] minor formatting cleanups --- specs/merge/fork-choice.md | 6 ++---- specs/phase0/beacon-chain.md | 1 - specs/phase0/fork-choice.md | 14 +++++++++----- specs/phase0/validator.md | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index bf67d2f4f..7a8db7dd9 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -176,10 +176,8 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: store.block_states[hash_tree_root(block)] = state # Add proposer score boost if the block is timely - is_before_attestation_broadcast = ( - store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT - ) - if get_current_slot(store) == block.slot and is_before_attestation_broadcast: + is_before_attesting_interval = store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // INTERVALS_PER_SLOT + if get_current_slot(store) == block.slot and is_before_attesting_interval: store.proposer_score_boost = LatestMessage( root=hash_tree_root(block), epoch=compute_epoch_at_slot(block.slot) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index e2e235acf..5216027cd 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -169,7 +169,6 @@ We define the following Python custom types for type hinting and readability: | `BLSPubkey` | `Bytes48` | a BLS12-381 public key | | `BLSSignature` | `Bytes96` | a BLS12-381 signature | - ## Constants The following values are (non-configurable) constants used throughout the specification. diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index f302887ec..7a87d0070 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -57,12 +57,18 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass 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). + +### Constant + +| Name | Value | +| - | - | +| `INTERVALS_PER_SLOT` | `uint64(3)` | + ### Preset | Name | Value | Unit | Duration | | - | - | :-: | :-: | | `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds | -| `ATTESTATION_OFFSET_QUOTIENT` | `uint64(3)` | - | - | ### Configuration @@ -406,10 +412,8 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: store.block_states[hash_tree_root(block)] = state # Add proposer score boost if the block is timely - is_before_attestation_broadcast = ( - store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT - ) - if get_current_slot(store) == block.slot and is_before_attestation_broadcast: + is_before_attesting_interval = store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // INTERVALS_PER_SLOT + if get_current_slot(store) == block.slot and is_before_attesting_interval: store.proposer_score_boost = LatestMessage( root=hash_tree_root(block), epoch=compute_epoch_at_slot(block.slot) diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index 209cc7e5d..e21ff980a 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -446,7 +446,7 @@ def get_block_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> A validator is expected to create, sign, and broadcast an attestation during each epoch. The `committee`, assigned `index`, and assigned `slot` for which the validator performs this role during an epoch are defined by `get_committee_assignment(state, epoch, validator_index)`. -A validator should create and broadcast the `attestation` to the associated attestation subnet when either (a) the validator has received a valid block from the expected block proposer for the assigned `slot` or (b) `1 / ATTESTATION_OFFSET_QUOTIENT` of the `slot` has transpired (`SECONDS_PER_SLOT / ATTESTATION_OFFSET_QUOTIENT` seconds after the start of `slot`) -- whichever comes _first_. +A validator should create and broadcast the `attestation` to the associated attestation subnet when either (a) the validator has received a valid block from the expected block proposer for the assigned `slot` or (b) `1 / INTERVALS_PER_SLOT` of the `slot` has transpired (`SECONDS_PER_SLOT / INTERVALS_PER_SLOT` seconds after the start of `slot`) -- whichever comes _first_. *Note*: Although attestations during `GENESIS_EPOCH` do not count toward FFG finality, these initial attestations do give weight to the fork choice, are rewarded, and should be made. @@ -569,7 +569,7 @@ def get_aggregate_signature(attestations: Sequence[Attestation]) -> BLSSignature #### Broadcast aggregate -If the validator is selected to aggregate (`is_aggregator`), then they broadcast their best aggregate as a `SignedAggregateAndProof` to the global aggregate channel (`beacon_aggregate_and_proof`) `2 / ATTESTATION_OFFSET_QUOTIENT` of the way through the `slot`-that is, `SECONDS_PER_SLOT * 2 / ATTESTATION_OFFSET_QUOTIENT` seconds after the start of `slot`. +If the validator is selected to aggregate (`is_aggregator`), then they broadcast their best aggregate as a `SignedAggregateAndProof` to the global aggregate channel (`beacon_aggregate_and_proof`) `2 / INTERVALS_PER_SLOT` of the way through the `slot`-that is, `SECONDS_PER_SLOT * 2 / INTERVALS_PER_SLOT` seconds after the start of `slot`. Selection proofs are provided in `AggregateAndProof` to prove to the gossip channel that the validator has been selected as an aggregator. From 282d85b9e7ea3148c7bae80a11c375d3e7497ecd Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 22 Nov 2021 11:31:44 -0700 Subject: [PATCH 10/20] simplify on_tick proposer boost update --- specs/phase0/fork-choice.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 7a87d0070..420ae5722 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -369,10 +369,11 @@ 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)) + if current_slot > previous_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 From ea09df50db2d764825ebcc9eddc468b48440c1a2 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 22 Nov 2021 11:33:03 -0700 Subject: [PATCH 11/20] toc --- specs/phase0/fork-choice.md | 1 + .../pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 420ae5722..9c27a423f 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -7,6 +7,7 @@ - [Introduction](#introduction) - [Fork choice](#fork-choice) + - [Constant](#constant) - [Preset](#preset) - [Configuration](#configuration) - [Helpers](#helpers) 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 6cf04559b..c4c9fffc1 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 @@ -724,7 +724,7 @@ def test_proposer_score_boost_same_slot_untimely_block(spec, state): # 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) + spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT) yield from tick_and_add_block(spec, store, signed_block, test_steps) assert store.proposer_score_boost.root == spec.Root() From 1d835c5198aa744a1ce64e7c61c0f61da421cda3 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Mon, 22 Nov 2021 14:44:52 -0800 Subject: [PATCH 12/20] Apply Danny's code review & suggestions --- specs/merge/fork-choice.md | 5 +- specs/phase0/fork-choice.md | 19 +++---- .../test/phase0/fork_choice/test_on_block.py | 53 ++++++++++++++++++- 3 files changed, 60 insertions(+), 17 deletions(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index 7a8db7dd9..cbfe33f4a 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -178,10 +178,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Add proposer score boost if the block is timely is_before_attesting_interval = store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // INTERVALS_PER_SLOT if get_current_slot(store) == block.slot and is_before_attesting_interval: - store.proposer_score_boost = LatestMessage( - root=hash_tree_root(block), - epoch=compute_epoch_at_slot(block.slot) - ) + store.proposer_boost_root = hash_tree_root(block) # Update justified checkpoint if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 9c27a423f..6e50b99d4 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -100,7 +100,7 @@ class Store(object): justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint best_justified_checkpoint: Checkpoint - proposer_score_boost: LatestMessage + proposer_boost_root: Root 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) @@ -121,14 +121,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)) + proposer_boost_root = Root() 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, + proposer_boost_root=proposer_boost_root, blocks={anchor_root: copy(anchor_block)}, block_states={anchor_root: copy(anchor_state)}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, @@ -182,9 +182,9 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: 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(): + if store.proposer_boost_root != Root(): block = store.blocks[root] - if get_ancestor(store, root, block.slot) == store.proposer_score_boost.root: + if get_ancestor(store, root, block.slot) == store.proposer_boost_root: num_validators = len(get_active_validator_indices(state, get_current_epoch(state))) avg_balance = get_total_active_balance(state) // num_validators committee_size = num_validators // SLOTS_PER_EPOCH @@ -371,9 +371,9 @@ def on_tick(store: Store, time: uint64) -> None: current_slot = get_current_slot(store) - # Reset store.proposer_score_boost if this is a new slot + # Reset store.proposer_boost_root if this is a new slot if current_slot > previous_slot: - store.proposer_score_boost = LatestMessage(root=Root(), epoch=Epoch(0)) + store.proposer_boost_root = Root() # Not a new epoch, return if not (current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0): @@ -416,10 +416,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Add proposer score boost if the block is timely is_before_attesting_interval = store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // INTERVALS_PER_SLOT if get_current_slot(store) == block.slot and is_before_attesting_interval: - store.proposer_score_boost = LatestMessage( - root=hash_tree_root(block), - epoch=compute_epoch_at_slot(block.slot) - ) + store.proposer_boost_root = hash_tree_root(block) # Update justified checkpoint if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch: 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 c4c9fffc1..f234125e0 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 @@ -707,7 +707,56 @@ def test_new_finalized_slot_is_justified_checkpoint_ancestor(spec, state): @with_all_phases @spec_state_test -def test_proposer_score_boost_same_slot_untimely_block(spec, state): +def test_proposer_boost(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 timely arrival just before end of boost interval + spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + + spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT - 1) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert store.proposer_boost_root == spec.hash_tree_root(block) + assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) > 0 + + # Ensure that boost is removed after slot is over + spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + + spec.config.SECONDS_PER_SLOT) + assert store.proposer_boost_root == spec.Root() + assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) == 0 + + 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 timely arrival at start of boost interval + spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT) + yield from tick_and_add_block(spec, store, signed_block, test_steps) + assert store.proposer_boost_root == spec.hash_tree_root(block) + assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) > 0 + + # Ensure that boost is removed after slot is over + spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + + spec.config.SECONDS_PER_SLOT) + assert store.proposer_boost_root == spec.Root() + assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) == 0 + + yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_proposer_boost_root_same_slot_untimely_block(spec, state): test_steps = [] genesis_state = state.copy() @@ -726,6 +775,6 @@ def test_proposer_score_boost_same_slot_untimely_block(spec, state): spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT) yield from tick_and_add_block(spec, store, signed_block, test_steps) - assert store.proposer_score_boost.root == spec.Root() + assert store.proposer_boost_root == spec.Root() yield 'steps', test_steps From d85d4399cbaf397b1c3e895618ace2da83b933d3 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Mon, 22 Nov 2021 14:45:20 -0800 Subject: [PATCH 13/20] Rename test --- .../test/phase0/fork_choice/test_get_head.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 bb9cbee2e..fccf1d636 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 @@ -271,7 +271,7 @@ def test_filtered_block_tree(spec, state): @with_all_phases @spec_state_test -def test_proposer_score_boost_basic(spec, state): +def test_proposer_boost_correct_head(spec, state): test_steps = [] genesis_state = state.copy() @@ -297,9 +297,8 @@ def test_proposer_score_boost_basic(spec, state): 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): + 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) @@ -309,19 +308,18 @@ def test_proposer_score_boost_basic(spec, state): # 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 store.proposer_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 store.proposer_boost_root == spec.hash_tree_root(block_1) 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 store.proposer_boost_root == spec.Root() assert spec.get_head(store) == spec.hash_tree_root(block_2) test_steps.append({ From 64b4ca2950a81076f5904fd111fe639ef3bf6749 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 22 Nov 2021 17:02:46 -0700 Subject: [PATCH 14/20] add PROPOSER_SCORE_BOOST to configuration yaml files --- configs/mainnet.yaml | 5 +++++ configs/minimal.yaml | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 6d23073b7..ce2697054 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -70,6 +70,11 @@ MIN_PER_EPOCH_CHURN_LIMIT: 4 CHURN_LIMIT_QUOTIENT: 65536 +# Fork choice +# --------------------------------------------------------------- +# 25% +PROPOSER_SCORE_BOOST: 25 + # Deposit contract # --------------------------------------------------------------- # Ethereum PoW Mainnet diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 4f99fce31..ef35b3a07 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -69,6 +69,12 @@ MIN_PER_EPOCH_CHURN_LIMIT: 4 CHURN_LIMIT_QUOTIENT: 32 +# Fork choice +# --------------------------------------------------------------- +# 25% +PROPOSER_SCORE_BOOST: 25 + + # Deposit contract # --------------------------------------------------------------- # Ethereum Goerli testnet From bdd7b0782ec72afd59e3cd188712b36f93aab863 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 23 Nov 2021 16:26:11 +0800 Subject: [PATCH 15/20] Add configuration value checks --- .../test/phase0/unittests/test_config_invariants.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/test_config_invariants.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/test_config_invariants.py index 3183904c8..8836d463e 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/unittests/test_config_invariants.py +++ b/tests/core/pyspec/eth2spec/test/phase0/unittests/test_config_invariants.py @@ -74,3 +74,10 @@ def test_time(spec, state): @spec_state_test def test_networking(spec, state): assert spec.RANDOM_SUBNETS_PER_VALIDATOR <= spec.ATTESTATION_SUBNET_COUNT + + +@with_all_phases +@spec_state_test +def test_fork_choice(spec, state): + assert spec.INTERVALS_PER_SLOT < spec.config.SECONDS_PER_SLOT + assert spec.config.PROPOSER_SCORE_BOOST <= 100 From a0b5a809d5513875ad7e2fcffab63110918eeece Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Tue, 23 Nov 2021 07:02:04 -0800 Subject: [PATCH 16/20] Apply HWW code's review - fix is_before_attesting_interval --- specs/merge/fork-choice.md | 3 ++- specs/phase0/fork-choice.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/specs/merge/fork-choice.md b/specs/merge/fork-choice.md index cbfe33f4a..dc392d263 100644 --- a/specs/merge/fork-choice.md +++ b/specs/merge/fork-choice.md @@ -176,7 +176,8 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: store.block_states[hash_tree_root(block)] = state # Add proposer score boost if the block is timely - is_before_attesting_interval = store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // INTERVALS_PER_SLOT + time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT + is_before_attesting_interval = time_into_slot < SECONDS_PER_SLOT // INTERVALS_PER_SLOT if get_current_slot(store) == block.slot and is_before_attesting_interval: store.proposer_boost_root = hash_tree_root(block) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 6e50b99d4..b6b7b0c83 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -414,7 +414,8 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: store.block_states[hash_tree_root(block)] = state # Add proposer score boost if the block is timely - is_before_attesting_interval = store.time % SECONDS_PER_SLOT < SECONDS_PER_SLOT // INTERVALS_PER_SLOT + time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT + is_before_attesting_interval = time_into_slot < SECONDS_PER_SLOT // INTERVALS_PER_SLOT if get_current_slot(store) == block.slot and is_before_attesting_interval: store.proposer_boost_root = hash_tree_root(block) From ecbe9190b942a45f5e7eb814dfc84ade3f4269ac Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Tue, 23 Nov 2021 07:20:54 -0800 Subject: [PATCH 17/20] Apply HWW code's review - properly update test steps --- .../test/phase0/fork_choice/test_get_head.py | 10 ++++--- .../test/phase0/fork_choice/test_on_block.py | 29 +++++++++++-------- 2 files changed, 23 insertions(+), 16 deletions(-) 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 fccf1d636..83e342e08 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 @@ -304,21 +304,23 @@ def test_proposer_boost_correct_head(spec, state): 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) + time = store.genesis_time + block_1.slot * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step(spec, store, time, test_steps) # Process block_2 - yield from tick_and_add_block(spec, store, signed_block_2, test_steps) + yield from add_block(spec, store, signed_block_2, test_steps) assert store.proposer_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) + yield from add_block(spec, store, signed_block_1, test_steps) assert store.proposer_boost_root == spec.hash_tree_root(block_1) 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) + time = store.genesis_time + (block_1.slot + 1) * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step(spec, store, time, test_steps) assert store.proposer_boost_root == spec.Root() assert spec.get_head(store) == spec.hash_tree_root(block_2) 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 f234125e0..5c3ba89de 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 @@ -723,15 +723,17 @@ def test_proposer_boost(spec, state): signed_block = state_transition_and_sign_block(spec, state, block) # Process block on timely arrival just before end of boost interval - spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + - spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT - 1) - yield from tick_and_add_block(spec, store, signed_block, test_steps) + time = (store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + + spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT - 1) + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block, test_steps) assert store.proposer_boost_root == spec.hash_tree_root(block) assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) > 0 # Ensure that boost is removed after slot is over - spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + - spec.config.SECONDS_PER_SLOT) + time = (store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + + spec.config.SECONDS_PER_SLOT) + on_tick_and_append_step(spec, store, time, test_steps) assert store.proposer_boost_root == spec.Root() assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) == 0 @@ -740,14 +742,16 @@ def test_proposer_boost(spec, state): signed_block = state_transition_and_sign_block(spec, state, block) # Process block on timely arrival at start of boost interval - spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT) - yield from tick_and_add_block(spec, store, signed_block, test_steps) + time = (store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT) + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block, test_steps) assert store.proposer_boost_root == spec.hash_tree_root(block) assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) > 0 # Ensure that boost is removed after slot is over - spec.on_tick(store, store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + - spec.config.SECONDS_PER_SLOT) + time = (store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + + spec.config.SECONDS_PER_SLOT) + on_tick_and_append_step(spec, store, time, test_steps) assert store.proposer_boost_root == spec.Root() assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) == 0 @@ -772,9 +776,10 @@ def test_proposer_boost_root_same_slot_untimely_block(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.INTERVALS_PER_SLOT) - yield from tick_and_add_block(spec, store, signed_block, test_steps) + time = (store.genesis_time + block.slot * spec.config.SECONDS_PER_SLOT + + spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT) + on_tick_and_append_step(spec, store, time, test_steps) + yield from add_block(spec, store, signed_block, test_steps) assert store.proposer_boost_root == spec.Root() yield 'steps', test_steps From 2a5c9d8dc5560a8ec5e784b4ed43569d7443d655 Mon Sep 17 00:00:00 2001 From: Aditya Asgaonkar Date: Tue, 23 Nov 2021 07:23:59 -0800 Subject: [PATCH 18/20] Set PROPOSER_SCORE_BOOST to 70% --- configs/mainnet.yaml | 2 +- configs/minimal.yaml | 2 +- specs/phase0/fork-choice.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index ce2697054..0a207b423 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -73,7 +73,7 @@ CHURN_LIMIT_QUOTIENT: 65536 # Fork choice # --------------------------------------------------------------- # 25% -PROPOSER_SCORE_BOOST: 25 +PROPOSER_SCORE_BOOST: 70 # Deposit contract # --------------------------------------------------------------- diff --git a/configs/minimal.yaml b/configs/minimal.yaml index ef35b3a07..508587ee5 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -72,7 +72,7 @@ CHURN_LIMIT_QUOTIENT: 32 # Fork choice # --------------------------------------------------------------- # 25% -PROPOSER_SCORE_BOOST: 25 +PROPOSER_SCORE_BOOST: 70 # Deposit contract diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index b6b7b0c83..d082ede30 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -75,7 +75,7 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass | Name | Value | | - | - | -| `PROPOSER_SCORE_BOOST` | `uint64(25)` | +| `PROPOSER_SCORE_BOOST` | `uint64(70)` | - The proposer score boost is worth `PROPOSER_SCORE_BOOST` percentage of the committee's weight, i.e., for slot with committee weight `committee_weight` the boost weight is equal to `(committee_weight * PROPOSER_SCORE_BOOST) // 100`. From 2ba0586c3de2e6bf1d2763366c822e923269dc40 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Wed, 24 Nov 2021 00:09:48 +0800 Subject: [PATCH 19/20] Add `proposer_boost_root` field to test vector "checks" step --- tests/core/pyspec/eth2spec/test/helpers/fork_choice.py | 1 + tests/formats/fork_choice/README.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index 098c05bf9..2fe0de90d 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -157,6 +157,7 @@ def add_block(spec, 'epoch': int(store.best_justified_checkpoint.epoch), 'root': encode_hex(store.best_justified_checkpoint.root), }, + 'proposer_boost_root': encode_hex(store.proposer_boost_root), } }) diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index 48dde2fb1..e4da31a9b 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -110,6 +110,7 @@ best_justified_checkpoint: { epoch: int, -- Integer value from store.best_justified_checkpoint.epoch root: string, -- Encoded 32-byte value from store.best_justified_checkpoint.root } +proposer_boost_root: string -- Encoded 32-byte value from store.proposer_boost_root ``` For example: @@ -120,6 +121,7 @@ For example: justified_checkpoint: {epoch: 3, root: '0xc25faab4acab38d3560864ca01e4d5cc4dc2cd473da053fbc03c2669143a2de4'} finalized_checkpoint: {epoch: 2, root: '0x40d32d6283ec11c53317a46808bc88f55657d93b95a1af920403187accf48f4f'} best_justified_checkpoint: {epoch: 3, root: '0xc25faab4acab38d3560864ca01e4d5cc4dc2cd473da053fbc03c2669143a2de4'} + proposer_boost_root: '0xdaa1d49d57594ced0c35688a6da133abb086d191a2ebdfd736fad95299325aeb' ``` *Note*: Each `checks` step may include one or multiple items. Each item has to be checked against the current store. From 975931b5cfc26f643221c3e043d23de539dd60e7 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 23 Nov 2021 11:19:22 -0700 Subject: [PATCH 20/20] pr feedback --- .../test/phase0/fork_choice/test_get_head.py | 7 ++++--- .../test/phase0/fork_choice/test_on_block.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) 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 83e342e08..d2c84fce7 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 @@ -117,10 +117,11 @@ def test_split_tie_breaker_no_attestations(spec, state): 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) + time = store.genesis_time + (block_2.slot + 1) * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step(spec, store, time, test_steps) - 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) + yield from add_block(spec, store, signed_block_1, test_steps) + yield from add_block(spec, store, signed_block_2, test_steps) highest_root = max(spec.hash_tree_root(block_1), spec.hash_tree_root(block_2)) assert spec.get_head(store) == highest_root 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 5c3ba89de..f57522ad7 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 @@ -1,4 +1,5 @@ import random +from eth_utils import encode_hex from eth2spec.utils.ssz.ssz_impl import hash_tree_root from eth2spec.test.context import MINIMAL, spec_state_test, with_all_phases, with_presets @@ -755,6 +756,12 @@ def test_proposer_boost(spec, state): assert store.proposer_boost_root == spec.Root() assert spec.get_latest_attesting_balance(store, spec.hash_tree_root(block)) == 0 + test_steps.append({ + 'checks': { + 'proposer_boost_root': encode_hex(store.proposer_boost_root), + } + }) + yield 'steps', test_steps @@ -780,6 +787,13 @@ def test_proposer_boost_root_same_slot_untimely_block(spec, state): spec.config.SECONDS_PER_SLOT // spec.INTERVALS_PER_SLOT) on_tick_and_append_step(spec, store, time, test_steps) yield from add_block(spec, store, signed_block, test_steps) + assert store.proposer_boost_root == spec.Root() + test_steps.append({ + 'checks': { + 'proposer_boost_root': encode_hex(store.proposer_boost_root), + } + }) + yield 'steps', test_steps