Skip to content

Commit cebac4d

Browse files
authored
Modify get_proposer_head for Gloas (#5348)
This PR modifies `get_proposer_head` for Gloas. It changes its signature to receives and returns ForkChoiceNode. Relevant comments are changed accordingly. Additionally, this PR fixes the issue stated in #5305. It changes `is_parent_strong` to consider the PENDING parent node to count support for the parent node regardless of its payload status. cc @mkalinin @michaelsproul
1 parent 30aa65f commit cebac4d

5 files changed

Lines changed: 107 additions & 38 deletions

File tree

specs/gloas/fork-choice.md

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
- [Modified `is_head_late`](#modified-is_head_late)
4949
- [Modified `is_head_weak`](#modified-is_head_weak)
5050
- [Modified `is_parent_strong`](#modified-is_parent_strong)
51+
- [Modified `get_proposer_head`](#modified-get_proposer_head)
5152
- [`on_attestation` helpers](#on_attestation-helpers)
5253
- [Modified `validate_on_attestation`](#modified-validate_on_attestation)
5354
- [Modified `update_latest_messages`](#modified-update_latest_messages)
@@ -416,9 +417,8 @@ def is_previous_slot_payload_decision(store: Store, node: ForkChoiceNode) -> boo
416417

417418
*Note*: This function is called by the proposer to decide whether to build on
418419
top of the *empty* or *full* parent node. For a node from an earlier slot, it
419-
follows the payload status resolved by `get_head`. For a *full* node from the
420-
previous slot, it considers the PTC view on both payload timeliness and data
421-
availability.
420+
follows the node's payload status. For a *full* node from the previous slot, it
421+
considers the PTC view on both payload timeliness and data availability.
422422

423423
```python
424424
def should_build_on_full(store: Store, head: ForkChoiceNode) -> bool:
@@ -762,17 +762,82 @@ def is_head_weak(store: Store, head_root: Root) -> bool:
762762

763763
#### Modified `is_parent_strong`
764764

765+
*Note*: This function is modified to measure support for the parent beacon block
766+
root regardless of its payload status.
767+
765768
```python
766769
def is_parent_strong(store: Store, root: Root) -> bool:
767770
justified_state = store.checkpoint_states[store.justified_checkpoint]
768771
parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD)
769-
block = store.blocks[root]
770-
parent_payload_status = get_parent_payload_status(store, block)
771-
parent_node = ForkChoiceNode(root=block.parent_root, payload_status=parent_payload_status)
772+
parent_root = store.blocks[root].parent_root
773+
# [Modified in Gloas:EIP7732]
774+
parent_node = ForkChoiceNode(root=parent_root, payload_status=PAYLOAD_STATUS_PENDING)
772775
parent_weight = get_attestation_score(store, parent_node, justified_state)
773776
return parent_weight > parent_threshold
774777
```
775778

779+
#### Modified `get_proposer_head`
780+
781+
*Note*: This function is modified to preserve the payload status of the parent
782+
node when a proposer re-org is selected.
783+
784+
```python
785+
def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode:
786+
head_block = store.blocks[head_node.root]
787+
parent_root = head_block.parent_root
788+
parent_block = store.blocks[parent_root]
789+
# [Modified in Gloas:EIP7732]
790+
parent_payload_status = get_parent_payload_status(store, head_block)
791+
parent_node = ForkChoiceNode(root=parent_root, payload_status=parent_payload_status)
792+
793+
# Only re-org the head block if it arrived later than the attestation deadline.
794+
head_late = is_head_late(store, head_node.root)
795+
796+
# Do not re-org on an epoch boundary.
797+
epoch_boundary = is_epoch_boundary(slot)
798+
799+
# Ensure that the FFG information of the new head will be competitive with the current head.
800+
ffg_competitive = is_ffg_competitive(store, head_node.root, parent_root)
801+
802+
# Do not re-org if the chain is not finalizing with acceptable frequency.
803+
finalization_ok = is_finalization_ok(store, slot)
804+
805+
# Only re-org if we are proposing on-time.
806+
proposing_on_time = is_proposing_on_time(store)
807+
808+
# Only re-org a single slot at most.
809+
parent_slot_ok = parent_block.slot + 1 == head_block.slot
810+
current_time_ok = head_block.slot + 1 == slot
811+
single_slot_reorg = parent_slot_ok and current_time_ok
812+
813+
# Check that the head has few enough votes to be overpowered by our proposer boost.
814+
assert store.proposer_boost_root != head_node.root # ensure boost has worn off
815+
head_weak = is_head_weak(store, head_node.root)
816+
817+
# Check that the missing votes are assigned to the parent and not being hoarded.
818+
parent_strong = is_parent_strong(store, head_node.root)
819+
820+
# Re-org more aggressively if there is a proposer equivocation in the previous slot.
821+
proposer_equivocation = is_proposer_equivocation(store, head_node.root)
822+
823+
if all([
824+
head_late,
825+
epoch_boundary,
826+
ffg_competitive,
827+
finalization_ok,
828+
proposing_on_time,
829+
single_slot_reorg,
830+
head_weak,
831+
parent_strong,
832+
]):
833+
# We can re-org the current head by building upon its parent node.
834+
return parent_node
835+
elif all([head_weak, current_time_ok, proposer_equivocation]):
836+
return parent_node
837+
else:
838+
return head_node
839+
```
840+
776841
### `on_attestation` helpers
777842

778843
#### Modified `validate_on_attestation`

specs/gloas/validator.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,10 @@ def get_proposer_preferences_signature(
182182

183183
#### Constructing the `BeaconBlockBody`
184184

185-
Let `head = get_head(store)` be the parent block the proposer is building on,
186-
from which `state` was derived.
185+
Let `head = get_head(store)`. A proposer may set
186+
`head = get_proposer_head(store, head, slot)` if proposer re-orgs are
187+
implemented and enabled. Let `head` be the parent node the proposer builds on,
188+
from which `state` is derived.
187189

188190
##### Signed execution payload bid
189191

specs/phase0/fork-choice.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
- [`get_aggregate_due_ms`](#get_aggregate_due_ms)
4141
- [Proposer head and reorg helpers](#proposer-head-and-reorg-helpers)
4242
- [`is_head_late`](#is_head_late)
43-
- [`is_shuffling_stable`](#is_shuffling_stable)
43+
- [`is_epoch_boundary`](#is_epoch_boundary)
4444
- [`is_ffg_competitive`](#is_ffg_competitive)
4545
- [`is_finalization_ok`](#is_finalization_ok)
4646
- [`is_proposing_on_time`](#is_proposing_on_time)
@@ -587,10 +587,10 @@ def is_head_late(store: Store, head_root: Root) -> bool:
587587
return not store.block_timeliness[head_root]
588588
```
589589

590-
##### `is_shuffling_stable`
590+
##### `is_epoch_boundary`
591591

592592
```python
593-
def is_shuffling_stable(slot: Slot) -> bool:
593+
def is_epoch_boundary(slot: Slot) -> bool:
594594
return slot % SLOTS_PER_EPOCH != 0
595595
```
596596

@@ -638,7 +638,8 @@ def is_parent_strong(store: Store, root: Root) -> bool:
638638
justified_state = store.checkpoint_states[store.justified_checkpoint]
639639
parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD)
640640
parent_root = store.blocks[root].parent_root
641-
parent_weight = get_weight(store, ForkChoiceNode(root=parent_root))
641+
parent_node = ForkChoiceNode(root=parent_root)
642+
parent_weight = get_weight(store, parent_node)
642643
return parent_weight > parent_threshold
643644
```
644645

@@ -661,19 +662,20 @@ def is_proposer_equivocation(store: Store, root: Root) -> bool:
661662
##### `get_proposer_head`
662663

663664
```python
664-
def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root:
665-
head_block = store.blocks[head_root]
665+
def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode:
666+
head_block = store.blocks[head_node.root]
666667
parent_root = head_block.parent_root
667668
parent_block = store.blocks[parent_root]
669+
parent_node = ForkChoiceNode(root=parent_root)
668670

669671
# Only re-org the head block if it arrived later than the attestation deadline.
670-
head_late = is_head_late(store, head_root)
672+
head_late = is_head_late(store, head_node.root)
671673

672-
# Do not re-org on an epoch boundary where the proposer shuffling could change.
673-
shuffling_stable = is_shuffling_stable(slot)
674+
# Do not re-org on an epoch boundary.
675+
epoch_boundary = is_epoch_boundary(slot)
674676

675677
# Ensure that the FFG information of the new head will be competitive with the current head.
676-
ffg_competitive = is_ffg_competitive(store, head_root, parent_root)
678+
ffg_competitive = is_ffg_competitive(store, head_node.root, parent_root)
677679

678680
# Do not re-org if the chain is not finalizing with acceptable frequency.
679681
finalization_ok = is_finalization_ok(store, slot)
@@ -687,31 +689,31 @@ def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root:
687689
single_slot_reorg = parent_slot_ok and current_time_ok
688690

689691
# Check that the head has few enough votes to be overpowered by our proposer boost.
690-
assert store.proposer_boost_root != head_root # ensure boost has worn off
691-
head_weak = is_head_weak(store, head_root)
692+
assert store.proposer_boost_root != head_node.root # ensure boost has worn off
693+
head_weak = is_head_weak(store, head_node.root)
692694

693695
# Check that the missing votes are assigned to the parent and not being hoarded.
694-
parent_strong = is_parent_strong(store, head_root)
696+
parent_strong = is_parent_strong(store, head_node.root)
695697

696698
# Re-org more aggressively if there is a proposer equivocation in the previous slot.
697-
proposer_equivocation = is_proposer_equivocation(store, head_root)
699+
proposer_equivocation = is_proposer_equivocation(store, head_node.root)
698700

699701
if all([
700702
head_late,
701-
shuffling_stable,
703+
epoch_boundary,
702704
ffg_competitive,
703705
finalization_ok,
704706
proposing_on_time,
705707
single_slot_reorg,
706708
head_weak,
707709
parent_strong,
708710
]):
709-
# We can re-org the current head by building upon its parent block.
710-
return parent_root
711+
# We can re-org the current head by building upon its parent node.
712+
return parent_node
711713
elif all([head_weak, current_time_ok, proposer_equivocation]):
712-
return parent_root
714+
return parent_node
713715
else:
714-
return head_root
716+
return head_node
715717
```
716718

717719
*Note*: The ordering of conditions is a suggestion only. Implementations are

specs/phase0/validator.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,13 +375,13 @@ To propose, the validator selects a `BeaconBlock`, `parent` using this process:
375375

376376
1. Compute fork choice's view of the head at the start of `slot`, after running
377377
`on_tick` and applying any queued attestations from `slot - 1`. Set
378-
`head_root = get_head(store).root`.
378+
`head_node = get_head(store)`.
379379
2. Compute the _proposer head_, which is the head upon which the proposer SHOULD
380380
build in order to incentivise timely block propagation by other validators.
381-
Set `parent_root = get_proposer_head(store, head_root, slot)`. A proposer may
382-
set `parent_root == head_root` if proposer re-orgs are not implemented or
381+
Set `parent_node = get_proposer_head(store, head_node, slot)`. A proposer may
382+
set `parent_node == head_node` if proposer re-orgs are not implemented or
383383
have been disabled.
384-
3. Let `parent` be the block with `parent_root`.
384+
3. Let `parent` be the block with `parent_node.root`.
385385

386386
The validator creates, signs, and broadcasts a `block` that is a child of
387387
`parent` and satisfies a valid

tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_proposer_head.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ def test_basic_is_head_root(spec, state):
5555

5656
current_time = slot * spec.config.SLOT_DURATION_MS // 1000 + store.genesis_time
5757
on_tick_and_append_step(spec, store, current_time, test_steps)
58-
proposer_head = spec.get_proposer_head(store, head.root, slot)
59-
assert proposer_head == head.root
58+
proposer_head = spec.get_proposer_head(store, head, slot)
59+
assert proposer_head.root == head.root
6060

6161
output_store_checks(spec, store, test_steps)
6262
test_steps.append(
6363
{
6464
"checks": {
65-
"get_proposer_head": encode_hex(proposer_head),
65+
"get_proposer_head": encode_hex(proposer_head.root),
6666
}
6767
}
6868
)
@@ -155,7 +155,7 @@ def test_basic_is_parent_root(spec, state):
155155

156156
# The conditions in `get_proposer_head`
157157
assert spec.is_head_late(store, head.root)
158-
assert spec.is_shuffling_stable(slot)
158+
assert spec.is_epoch_boundary(slot)
159159
assert spec.is_ffg_competitive(store, head.root, parent_root)
160160
assert spec.is_finalization_ok(store, slot)
161161
assert spec.is_proposing_on_time(store)
@@ -168,14 +168,14 @@ def test_basic_is_parent_root(spec, state):
168168
assert spec.is_head_weak(store, head.root)
169169
assert spec.is_parent_strong(store, head.root)
170170

171-
proposer_head = spec.get_proposer_head(store, head.root, state.slot)
172-
assert proposer_head == parent_root
171+
proposer_head = spec.get_proposer_head(store, head, state.slot)
172+
assert proposer_head.root == parent_root
173173

174174
output_store_checks(spec, store, test_steps)
175175
test_steps.append(
176176
{
177177
"checks": {
178-
"get_proposer_head": encode_hex(proposer_head),
178+
"get_proposer_head": encode_hex(proposer_head.root),
179179
}
180180
}
181181
)

0 commit comments

Comments
 (0)