From 0893420f153437554e98e04ec1d46904b1118632 Mon Sep 17 00:00:00 2001 From: Jihoon Song Date: Mon, 8 Jun 2026 19:07:20 +0900 Subject: [PATCH 1/6] Modify Gloas's `is_parent_strong` to use PENDING parent --- specs/gloas/fork-choice.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/specs/gloas/fork-choice.md b/specs/gloas/fork-choice.md index d8e79d7f16..6a507ec2a5 100644 --- a/specs/gloas/fork-choice.md +++ b/specs/gloas/fork-choice.md @@ -762,13 +762,16 @@ def is_head_weak(store: Store, head_root: Root) -> bool: #### Modified `is_parent_strong` +*Note*: This function is modified to measure support for the parent beacon block +root regardless of its payload status. + ```python def is_parent_strong(store: Store, root: Root) -> bool: justified_state = store.checkpoint_states[store.justified_checkpoint] parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD) - block = store.blocks[root] - parent_payload_status = get_parent_payload_status(store, block) - parent_node = ForkChoiceNode(root=block.parent_root, payload_status=parent_payload_status) + parent_root = store.blocks[root].parent_root + # [Modified in Gloas:EIP7732] + parent_node = ForkChoiceNode(root=parent_root, payload_status=PAYLOAD_STATUS_PENDING) parent_weight = get_attestation_score(store, parent_node, justified_state) return parent_weight > parent_threshold ``` From 5476ca0364e451b98fa54f665e7823533d4c8dce Mon Sep 17 00:00:00 2001 From: Jihoon Song Date: Mon, 8 Jun 2026 19:07:31 +0900 Subject: [PATCH 2/6] Align `is_parent_strong` bodies in Phase0 and Gloas --- specs/phase0/fork-choice.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 275b36991f..d1f917ee23 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -638,7 +638,8 @@ def is_parent_strong(store: Store, root: Root) -> bool: justified_state = store.checkpoint_states[store.justified_checkpoint] parent_threshold = calculate_committee_fraction(justified_state, REORG_PARENT_WEIGHT_THRESHOLD) parent_root = store.blocks[root].parent_root - parent_weight = get_weight(store, ForkChoiceNode(root=parent_root)) + parent_node = ForkChoiceNode(root=parent_root) + parent_weight = get_weight(store, parent_node) return parent_weight > parent_threshold ``` From c17fbb584eb20724e19cf8fccd39ca5f85e5b548 Mon Sep 17 00:00:00 2001 From: Jihoon Song Date: Tue, 9 Jun 2026 00:12:27 +0900 Subject: [PATCH 3/6] Modify `get_proposer_head` to receive and return ForkChoiceNode --- specs/gloas/fork-choice.md | 63 +++++++++++++++++++++++++++++++++++++ specs/phase0/fork-choice.md | 25 ++++++++------- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/specs/gloas/fork-choice.md b/specs/gloas/fork-choice.md index 6a507ec2a5..82364bc4a4 100644 --- a/specs/gloas/fork-choice.md +++ b/specs/gloas/fork-choice.md @@ -48,6 +48,7 @@ - [Modified `is_head_late`](#modified-is_head_late) - [Modified `is_head_weak`](#modified-is_head_weak) - [Modified `is_parent_strong`](#modified-is_parent_strong) + - [Modified `get_proposer_head`](#modified-get_proposer_head) - [`on_attestation` helpers](#on_attestation-helpers) - [Modified `validate_on_attestation`](#modified-validate_on_attestation) - [Modified `update_latest_messages`](#modified-update_latest_messages) @@ -776,6 +777,68 @@ def is_parent_strong(store: Store, root: Root) -> bool: return parent_weight > parent_threshold ``` +#### Modified `get_proposer_head` + +*Note*: This function is modified to preserve the payload status of the parent +node when a proposer re-org is selected. + +```python +def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: + head_block = store.blocks[head_node.root] + parent_root = head_block.parent_root + parent_block = store.blocks[parent_root] + # [Modified in Gloas:EIP7732] + parent_payload_status = get_parent_payload_status(store, head_block) + parent_node = ForkChoiceNode(root=parent_root, payload_status=parent_payload_status) + + # Only re-org the head block if it arrived later than the attestation deadline. + head_late = is_head_late(store, head_node.root) + + # Do not re-org on an epoch boundary where the proposer shuffling could change. + shuffling_stable = is_shuffling_stable(slot) + + # Ensure that the FFG information of the new head will be competitive with the current head. + ffg_competitive = is_ffg_competitive(store, head_node.root, parent_root) + + # Do not re-org if the chain is not finalizing with acceptable frequency. + finalization_ok = is_finalization_ok(store, slot) + + # Only re-org if we are proposing on-time. + proposing_on_time = is_proposing_on_time(store) + + # Only re-org a single slot at most. + parent_slot_ok = parent_block.slot + 1 == head_block.slot + current_time_ok = head_block.slot + 1 == slot + single_slot_reorg = parent_slot_ok and current_time_ok + + # Check that the head has few enough votes to be overpowered by our proposer boost. + assert store.proposer_boost_root != head_node.root # ensure boost has worn off + head_weak = is_head_weak(store, head_node.root) + + # Check that the missing votes are assigned to the parent and not being hoarded. + parent_strong = is_parent_strong(store, head_node.root) + + # Re-org more aggressively if there is a proposer equivocation in the previous slot. + proposer_equivocation = is_proposer_equivocation(store, head_node.root) + + if all([ + head_late, + shuffling_stable, + ffg_competitive, + finalization_ok, + proposing_on_time, + single_slot_reorg, + head_weak, + parent_strong, + ]): + # We can re-org the current head by building upon its parent node. + return parent_node + elif all([head_weak, current_time_ok, proposer_equivocation]): + return parent_node + else: + return head_node +``` + ### `on_attestation` helpers #### Modified `validate_on_attestation` diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index d1f917ee23..0560a98e9c 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -662,19 +662,20 @@ def is_proposer_equivocation(store: Store, root: Root) -> bool: ##### `get_proposer_head` ```python -def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root: - head_block = store.blocks[head_root] +def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: + head_block = store.blocks[head_node.root] parent_root = head_block.parent_root parent_block = store.blocks[parent_root] + parent_node = ForkChoiceNode(root=parent_root) # Only re-org the head block if it arrived later than the attestation deadline. - head_late = is_head_late(store, head_root) + head_late = is_head_late(store, head_node.root) # Do not re-org on an epoch boundary where the proposer shuffling could change. shuffling_stable = is_shuffling_stable(slot) # Ensure that the FFG information of the new head will be competitive with the current head. - ffg_competitive = is_ffg_competitive(store, head_root, parent_root) + ffg_competitive = is_ffg_competitive(store, head_node.root, parent_root) # Do not re-org if the chain is not finalizing with acceptable frequency. finalization_ok = is_finalization_ok(store, slot) @@ -688,14 +689,14 @@ def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root: single_slot_reorg = parent_slot_ok and current_time_ok # Check that the head has few enough votes to be overpowered by our proposer boost. - assert store.proposer_boost_root != head_root # ensure boost has worn off - head_weak = is_head_weak(store, head_root) + assert store.proposer_boost_root != head_node.root # ensure boost has worn off + head_weak = is_head_weak(store, head_node.root) # Check that the missing votes are assigned to the parent and not being hoarded. - parent_strong = is_parent_strong(store, head_root) + parent_strong = is_parent_strong(store, head_node.root) # Re-org more aggressively if there is a proposer equivocation in the previous slot. - proposer_equivocation = is_proposer_equivocation(store, head_root) + proposer_equivocation = is_proposer_equivocation(store, head_node.root) if all([ head_late, @@ -707,12 +708,12 @@ def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root: head_weak, parent_strong, ]): - # We can re-org the current head by building upon its parent block. - return parent_root + # We can re-org the current head by building upon its parent node. + return parent_node elif all([head_weak, current_time_ok, proposer_equivocation]): - return parent_root + return parent_node else: - return head_root + return head_node ``` *Note*: The ordering of conditions is a suggestion only. Implementations are From 693991c746327b1e3eb12e53ffc075fcd3a1770f Mon Sep 17 00:00:00 2001 From: Jihoon Song Date: Tue, 9 Jun 2026 00:55:32 +0900 Subject: [PATCH 4/6] Modify `get_proposer_head`-related honest validator comments --- specs/gloas/validator.md | 6 ++++-- specs/phase0/validator.md | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/specs/gloas/validator.md b/specs/gloas/validator.md index 87f907618c..c284e30422 100644 --- a/specs/gloas/validator.md +++ b/specs/gloas/validator.md @@ -182,8 +182,10 @@ def get_proposer_preferences_signature( #### Constructing the `BeaconBlockBody` -Let `head = get_head(store)` be the parent block the proposer is building on, -from which `state` was derived. +Let `head = get_head(store)`. A proposer may set +`head = get_proposer_head(store, head, slot)` if proposer re-orgs are +implemented and enabled. Let `head` be the parent node the proposer builds on, +from which `state` is derived. ##### Signed execution payload bid diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index c53cc215b4..96c5123310 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -375,13 +375,13 @@ To propose, the validator selects a `BeaconBlock`, `parent` using this process: 1. Compute fork choice's view of the head at the start of `slot`, after running `on_tick` and applying any queued attestations from `slot - 1`. Set - `head_root = get_head(store).root`. + `head_node = get_head(store)`. 2. Compute the _proposer head_, which is the head upon which the proposer SHOULD build in order to incentivise timely block propagation by other validators. - Set `parent_root = get_proposer_head(store, head_root, slot)`. A proposer may - set `parent_root == head_root` if proposer re-orgs are not implemented or + Set `parent_node = get_proposer_head(store, head_node, slot)`. A proposer may + set `parent_node == head_node` if proposer re-orgs are not implemented or have been disabled. -3. Let `parent` be the block with `parent_root`. +3. Let `parent` be the block with `parent_node.root`. The validator creates, signs, and broadcasts a `block` that is a child of `parent` and satisfies a valid From 28fba8760b1469e12d5e9a16295d53073d0e02c6 Mon Sep 17 00:00:00 2001 From: Jihoon Song Date: Tue, 9 Jun 2026 01:02:06 +0900 Subject: [PATCH 5/6] Modify `should_build_on_full`'s note comment --- specs/gloas/fork-choice.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/specs/gloas/fork-choice.md b/specs/gloas/fork-choice.md index 82364bc4a4..d59eea7469 100644 --- a/specs/gloas/fork-choice.md +++ b/specs/gloas/fork-choice.md @@ -417,9 +417,8 @@ def is_previous_slot_payload_decision(store: Store, node: ForkChoiceNode) -> boo *Note*: This function is called by the proposer to decide whether to build on top of the *empty* or *full* parent node. For a node from an earlier slot, it -follows the payload status resolved by `get_head`. For a *full* node from the -previous slot, it considers the PTC view on both payload timeliness and data -availability. +follows the node's payload status. For a *full* node from the previous slot, it +considers the PTC view on both payload timeliness and data availability. ```python def should_build_on_full(store: Store, head: ForkChoiceNode) -> bool: From 24a68ea22cd968efdefde700851997c7b45af0dd Mon Sep 17 00:00:00 2001 From: Jihoon Song Date: Tue, 9 Jun 2026 01:14:06 +0900 Subject: [PATCH 6/6] Modify `get_proposer_head`-related tests --- .../phase0/fork_choice/test_get_proposer_head.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_proposer_head.py b/tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_proposer_head.py index cd602a2532..dfd634eb96 100644 --- a/tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_proposer_head.py +++ b/tests/core/pyspec/eth_consensus_specs/test/phase0/fork_choice/test_get_proposer_head.py @@ -55,14 +55,14 @@ def test_basic_is_head_root(spec, state): current_time = slot * spec.config.SLOT_DURATION_MS // 1000 + store.genesis_time on_tick_and_append_step(spec, store, current_time, test_steps) - proposer_head = spec.get_proposer_head(store, head.root, slot) - assert proposer_head == head.root + proposer_head = spec.get_proposer_head(store, head, slot) + assert proposer_head.root == head.root output_store_checks(spec, store, test_steps) test_steps.append( { "checks": { - "get_proposer_head": encode_hex(proposer_head), + "get_proposer_head": encode_hex(proposer_head.root), } } ) @@ -168,14 +168,14 @@ def test_basic_is_parent_root(spec, state): assert spec.is_head_weak(store, head.root) assert spec.is_parent_strong(store, head.root) - proposer_head = spec.get_proposer_head(store, head.root, state.slot) - assert proposer_head == parent_root + proposer_head = spec.get_proposer_head(store, head, state.slot) + assert proposer_head.root == parent_root output_store_checks(spec, store, test_steps) test_steps.append( { "checks": { - "get_proposer_head": encode_hex(proposer_head), + "get_proposer_head": encode_hex(proposer_head.root), } } )