From 2c3a6f5e8434313e690acf10f95d20dae9e5d989 Mon Sep 17 00:00:00 2001 From: Caleb Date: Tue, 16 Dec 2025 22:48:28 +0000 Subject: [PATCH 1/4] extend data structures for gloas forkchoice --- .../fork_choice/fork_choice_types.nim | 29 ++++++++++++++++++- beacon_chain/spec/datatypes/gloas.nim | 10 ------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/beacon_chain/fork_choice/fork_choice_types.nim b/beacon_chain/fork_choice/fork_choice_types.nim index 743ec59958..c98e2f4d61 100644 --- a/beacon_chain/fork_choice/fork_choice_types.nim +++ b/beacon_chain/fork_choice/fork_choice_types.nim @@ -126,28 +126,55 @@ type current_root*: Eth2Digest next_root*: Eth2Digest next_epoch*: Epoch + next_slot*: Slot + payload_present*: bool ForkChoiceBackend* = object proto_array*: ProtoArray votes*: seq[VoteTracker] balances*: seq[Gwei] + # Additional state tracking for Gloas + execution_payload_states*: Table[Eth2Digest, Eth2Digest] # root -> state_root + ptc_vote*: Table[Eth2Digest, seq[bool]] QueuedAttestation* = object slot*: Slot attesting_indices*: seq[ValidatorIndex] block_root*: Eth2Digest target_epoch*: Epoch + # Gloas - track committee index for payload preference + committee_index*: CommitteeIndex ForkChoice* = object backend*: ForkChoiceBackend checkpoints*: Checkpoints queuedAttestations*: seq[QueuedAttestation] +# New Fork choice types for Gloas +# ---------------------------------------------------------------------- + +type + # https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/gloas/fork-choice.md#custom-types + PayloadStatus* = uint8 + + ForkChoiceNode* = object + root*: Eth2Digest + payloadStatus*: PayloadStatus + +const + # https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/gloas/fork-choice.md#constants + PAYLOAD_TIMELY_THRESHOLD*: uint64 = PTC_SIZE div 2 + PAYLOAD_STATUS_PENDING* = PayloadStatus(0) + PAYLOAD_STATUS_EMPTY* = PayloadStatus(1) + PAYLOAD_STATUS_FULL* = PayloadStatus(2) + func shortLog*(vote: VoteTracker): auto = ( current_root: shortLog(vote.current_root), next_root: shortLog(vote.next_root), - next_epoch: vote.next_epoch + next_epoch: vote.next_epoch, + next_slot: vote.next_slot, + payload_present: vote.payload_present ) chronicles.formatIt VoteTracker: it.shortLog diff --git a/beacon_chain/spec/datatypes/gloas.nim b/beacon_chain/spec/datatypes/gloas.nim index 3699c8742b..a7b8d4e7a2 100644 --- a/beacon_chain/spec/datatypes/gloas.nim +++ b/beacon_chain/spec/datatypes/gloas.nim @@ -36,20 +36,10 @@ from ./deneb import export json_serialization, base -type - # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/gloas/fork-choice.md#custom-types - PayloadStatus* = uint8 - const # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/gloas/beacon-chain.md#state-list-lengths BUILDER_PENDING_WITHDRAWALS_LIMIT*: uint64 = 1_048_576 - # https://github.com/ethereum/consensus-specs/blob/v1.6.0-alpha.6/specs/gloas/fork-choice.md#constants - PAYLOAD_TIMELY_THRESHOLD*: uint64 = PTC_SIZE div 2 - PAYLOAD_STATUS_PENDING* = PayloadStatus(0) - PAYLOAD_STATUS_EMPTY* = PayloadStatus(1) - PAYLOAD_STATUS_FULL* = PayloadStatus(2) - type # https://github.com/ethereum/consensus-specs/blob/v1.6.0-beta.1/specs/gloas/p2p-interface.md#modified-datacolumnsidecar DataColumnSidecar* = object From 90014187c24fd67b399c74c81bf4b6d8c4644d27 Mon Sep 17 00:00:00 2001 From: Caleb Date: Wed, 17 Dec 2025 22:08:13 +0000 Subject: [PATCH 2/4] update vote processing --- beacon_chain/fork_choice/fork_choice.nim | 49 ++++++++++---- tests/test_fork_choice_gloas.nim | 82 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 tests/test_fork_choice_gloas.nim diff --git a/beacon_chain/fork_choice/fork_choice.nim b/beacon_chain/fork_choice/fork_choice.nim index 7225677456..3580fbe8fe 100644 --- a/beacon_chain/fork_choice/fork_choice.nim +++ b/beacon_chain/fork_choice/fork_choice.nim @@ -43,6 +43,10 @@ func compute_deltas( old_balances: openArray[Gwei], new_balances: openArray[Gwei] ): FcResult[void] + +func isGloasEnabled(dag: ChainDAGRef, slot: Slot): bool = + slot.epoch >= dag.cfg.GLOAS_FORK_EPOCH + # Fork choice routines # ---------------------------------------------------------------------- @@ -152,25 +156,44 @@ proc on_tick( ok() -func process_attestation( +func process_attestation*( self: var ForkChoiceBackend, validator_index: ValidatorIndex, block_root: Eth2Digest, - target_epoch: Epoch + target_epoch: Epoch, + attestation_slot: Slot, + payload_present: bool, + cfg: RuntimeConfig ) = ## Add an attestation to the fork choice context self.votes.extend(validator_index.int + 1) template vote: untyped = self.votes[validator_index] - if target_epoch > vote.next_epoch or vote.next_root.isZero: - vote.next_root = block_root - vote.next_epoch = target_epoch + + if attestation_slot.epoch >= cfg.GLOAS_FORK_EPOCH: + # slot based tracking with payload preference + if attestation_slot > vote.next_slot or vote.next_root.isZero: + vote.next_root = block_root + vote.next_slot = attestation_slot + vote.next_epoch = target_epoch + vote.payload_present = payload_present + + trace "Integrating Gloas vote in fork choice", + validator_index = validator_index, + slot = attestation_slot, + payload_present = payload_present, + new_vote = shortLog(vote) + else: + if target_epoch > vote.next_epoch or vote.next_root.isZero: + vote.next_root = block_root + vote.next_epoch = target_epoch trace "Integrating vote in fork choice", validator_index = validator_index, new_vote = shortLog(vote) -proc process_attestation_queue(self: var ForkChoice, slot: Slot) = +proc process_attestation_queue( + self: var ForkChoice, slot: Slot, dag: ChainDAGRef) = # Spec: # Attestations can only affect the fork choice of subsequent slots. # Delay consideration in the fork choice until their slot is in the past. @@ -179,7 +202,8 @@ proc process_attestation_queue(self: var ForkChoice, slot: Slot) = if it.slot < slot: for validator_index in it.attesting_indices: self.backend.process_attestation( - validator_index, it.block_root, it.slot.epoch()) + validator_index, it.block_root, it.slot.epoch(), it.slot, + it.committee_index == 1, dag.cfg) false else: true @@ -210,7 +234,7 @@ proc update_time*( ? self.on_tick(dag, time) if preSlot != postSlot: - self.process_attestation_queue(postSlot) + self.process_attestation_queue(postSlot, dag) ok() @@ -221,6 +245,7 @@ proc on_attestation*( attestation_slot: Slot, beacon_block_root: Eth2Digest, attesting_indices: openArray[ValidatorIndex], + attestation_committee_index: CommitteeIndex, wallTime: BeaconTime ): FcResult[void] = ? self.update_time(dag, @@ -230,7 +255,8 @@ proc on_attestation*( for validator_index in attesting_indices: # attestation_slot and target epoch must match, per attestation rules self.backend.process_attestation( - validator_index, beacon_block_root, attestation_slot.epoch) + validator_index, beacon_block_root, attestation_slot.epoch, attestation_slot, + attestation_committee_index == 1, dag.cfg) else: # Spec: # Attestations can only affect the fork choice of subsequent slots. @@ -238,7 +264,8 @@ proc on_attestation*( self.queuedAttestations.add(QueuedAttestation( slot: attestation_slot, attesting_indices: @attesting_indices, - block_root: beacon_block_root)) + block_root: beacon_block_root, + committee_index: attestation_committee_index)) ok() # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/fork-choice.md#on_attester_slashing @@ -295,7 +322,7 @@ proc process_block*(self: var ForkChoice, # Add proposer score boost if the block is timely let slot = self.checkpoints.time.slotOrZero(dag.timeParams) if slot == blck.slot and - self.checkpoints.time < slot.attestation_deadline( + self.checkpoints.time <= slot.attestation_deadline( dag.timeParams, typeof(blck).kind) and self.checkpoints.proposer_boost_root == ZERO_HASH: self.checkpoints.proposer_boost_root = blckRef.root diff --git a/tests/test_fork_choice_gloas.nim b/tests/test_fork_choice_gloas.nim new file mode 100644 index 0000000000..6a5bc35034 --- /dev/null +++ b/tests/test_fork_choice_gloas.nim @@ -0,0 +1,82 @@ +# beacon_chain +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +{.push raises: [], gcsafe.} +{.used.} + +import + unittest2, + ../beacon_chain/spec/datatypes/base, + ".."/beacon_chain/fork_choice/[fork_choice_types, fork_choice] + +suite "Vote Processing - Slot Tracking": + + test "Gloas: Votes update based on slot": + var backend = ForkChoiceBackend() + backend.votes = newSeq[VoteTracker](10) + + var cfg = defaultRuntimeConfig + cfg.GLOAS_FORK_EPOCH = Epoch(0) + + let + validator_idx = ValidatorIndex(5) + block_root1 = Eth2Digest.fromHex("0x4444444444444444444444444444444444444444444444444444444444444444") + backend.process_attestation( + validator_idx, block_root1, Epoch(3), Slot(100), false, cfg) + + check: + backend.votes[5].next_slot == Slot(100) + backend.votes[5].next_root == block_root1 + backend.votes[5].payload_present == false + + # Try to update with OLDER slot - should NOT update + let block_root2 = Eth2Digest.fromHex("0x5555555555555555555555555555555555555555555555555555555555555555") + backend.process_attestation( + validator_idx, block_root2, Epoch(3), Slot(99), true, cfg) + + check: + backend.votes[5].next_slot == Slot(100) + backend.votes[5].next_root == block_root1 + + # Update with NEWER slot - should update + let block_root3 = Eth2Digest.fromHex("0x6666666666666666666666666666666666666666666666666666666666666666") + backend.process_attestation( + validator_idx, block_root3, Epoch(3), Slot(101), true, cfg) + + check: + backend.votes[5].next_slot == Slot(101) + backend.votes[5].next_root == block_root3 + backend.votes[5].payload_present == true + + test "Gloas: payload_present flag tracks EMPTY vs FULL preference": + # When attesting to a block from a previous slot, validators signal + # whether they prefer building on EMPTY or FULL branch. + var backend = ForkChoiceBackend() + backend.votes = newSeq[VoteTracker](10) + + var cfg = defaultRuntimeConfig + cfg.GLOAS_FORK_EPOCH = Epoch(0) + + let validator_idx = ValidatorIndex(3) + + # Vote preferring EMPTY branch (attestation.data.index = 0) + backend.process_attestation( + validator_idx, + Eth2Digest.fromHex("0x7777777777777777777777777777777777777777777777777777777777777777"), + Epoch(5), Slot(150), false, cfg) + + check: + backend.votes[3].payload_present == false + + # Later vote preferring FULL branch (attestation.data.index = 1) + backend.process_attestation( + validator_idx, + Eth2Digest.fromHex("0x8888888888888888888888888888888888888888888888888888888888888888"), + Epoch(5), Slot(151), true, cfg) + + check: + backend.votes[3].payload_present == true From c4017ed0d216534bdc86d24a1a4fa4d9a1752dc4 Mon Sep 17 00:00:00 2001 From: Caleb Date: Wed, 17 Dec 2025 23:26:42 +0000 Subject: [PATCH 3/4] payload timeliness and extension logic --- beacon_chain/fork_choice/fork_choice.nim | 117 ++++++++++++++++++ .../fork_choice/fork_choice_types.nim | 2 +- tests/test_fork_choice_gloas.nim | 92 +++++++++++++- 3 files changed, 204 insertions(+), 7 deletions(-) diff --git a/beacon_chain/fork_choice/fork_choice.nim b/beacon_chain/fork_choice/fork_choice.nim index 3580fbe8fe..9a5d968c38 100644 --- a/beacon_chain/fork_choice/fork_choice.nim +++ b/beacon_chain/fork_choice/fork_choice.nim @@ -499,6 +499,123 @@ func compute_deltas( vote.current_root = vote.next_root return ok() +# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/gloas/fork-choice.md#new-is_payload_timely +func is_payload_timely*(self: ForkChoiceBackend, root: Eth2Digest): bool = + ## Return whether the execution payload for the beacon block with root ``root`` + ## was voted as present by the PTC, and was locally determined to be available. + + # The beacon block root must be known + if root notin self.ptc_vote: + return false + + # If the payload is not locally available, the payload + # is not considered available regardless of the PTC vote + if root notin self.execution_payload_states: + return false + + let votes = self.ptc_vote.getOrDefault(root, @[]) + var vote_count = 0 + for vote in votes: + if vote: + inc vote_count + + if vote_count.uint64 > PAYLOAD_TIMELY_THRESHOLD: + trace "Payload crossed timeliness threshhold", + root = shortLog(root), + votes = vote_count, + threshhold = PAYLOAD_TIMELY_THRESHOLD + return true + false + +# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/gloas/fork-choice.md#new-on_payload_attestation_message +proc on_payload_attestation_message*( + self: var ForkChoice, + dag: ChainDAGRef, + validator_index: ValidatorIndex, + beacon_block_root: Eth2Digest, + slot: Slot, + payload_present: bool, + is_from_block: bool = false): FcResult[void] = + ## Run ``on_payload_attestation_message`` upon receiving a new ``ptc_message`` directly on the wire. + ## + ## This is called when we receive a PayloadAttestationMessage either: + ## - From gossip (is_from_block = false) + ## - From a block's body.payload_attestations (is_from_block = true) + ## + ## Block at slot N -> PTC votes during slot N -> votes ncluded in slot N+1 + + if not dag.isGloasEnabled(slot): + return ok() + + # The beacon block root must be known + if beacon_block_root notin self.backend.proto_array.indices: + return ok() + + # PTC attestation must be for a known block. If block is unknown, delay consideration until the block is found + if beacon_block_root notin self.backend.ptc_vote: + self.backend.ptc_vote[beacon_block_root] = newSeq[bool](PTC_SIZE) + + withState(dag.headState): + when consensusFork >= ConsensusFork.Gloas: + var + cache: StateCache + ptc_index = -1 + i = 0 + + for vidx in get_ptc(forkyState.data, slot, cache): + if vidx == validator_index: + ptc_index = i + break + inc i + + if ptc_index >= 0: + var votes = + self.backend.ptc_vote.mgetOrPut( + beacon_block_root, newSeq[bool](PTC_SIZE)) + + votes[ptc_index] = payload_present + + trace "Recorded PTC vote", + validator_index = validator_index, + beacon_block_root = shortLog(beacon_block_root), + slot = slot, + payload_present = payload_present, + ptc_index = ptc_index, + is_from_block = is_from_block + else: + debug "PTC vote from unknown validator index", + validator_index = validator_index, + slot = slot + else: + debug "Attempted to record PTC vote before GLOAS fork" + ok() + +# https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/gloas/fork-choice.md#new-on_execution_payload +proc on_execution_payload( + self: var ForkChoice, + dag: ChainDAGRef, + beacon_block_root: Eth2Digest, + execution_payload_state_root: Eth2Digest): FcResult[void] = + ## Run ``on_execution_payload`` upon receiving a new execution payload. + + let current_slot = self.checkpoints.time.slotOrZero(dag.timeParams) + if not dag.isGloasEnabled(current_slot): + return ok() + + # The corresponding beacon block root needs to be known + if beacon_block_root notin self.backend.proto_array.indices: + debug "Execution payload for unknown block", + beacon_block_root = shortLog(beacon_block_root) + return ok() + + self.backend.execution_payload_states[beacon_block_root] = + execution_payload_state_root + + debug "Recorded execution payload availability", + beacon_block_root = shortLog(beacon_block_root), + state_root = shortLog(execution_payload_state_root) + ok() + # Sanity checks # ---------------------------------------------------------------------- # Sanity checks on internal private procedures diff --git a/beacon_chain/fork_choice/fork_choice_types.nim b/beacon_chain/fork_choice/fork_choice_types.nim index c98e2f4d61..54fc634861 100644 --- a/beacon_chain/fork_choice/fork_choice_types.nim +++ b/beacon_chain/fork_choice/fork_choice_types.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2018-2024 Status Research & Development GmbH +# Copyright (c) 2018-2025 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). diff --git a/tests/test_fork_choice_gloas.nim b/tests/test_fork_choice_gloas.nim index 6a5bc35034..09a4ea55e3 100644 --- a/tests/test_fork_choice_gloas.nim +++ b/tests/test_fork_choice_gloas.nim @@ -14,15 +14,14 @@ import ".."/beacon_chain/fork_choice/[fork_choice_types, fork_choice] suite "Vote Processing - Slot Tracking": - test "Gloas: Votes update based on slot": var backend = ForkChoiceBackend() backend.votes = newSeq[VoteTracker](10) - + var cfg = defaultRuntimeConfig cfg.GLOAS_FORK_EPOCH = Epoch(0) - let + let validator_idx = ValidatorIndex(5) block_root1 = Eth2Digest.fromHex("0x4444444444444444444444444444444444444444444444444444444444444444") backend.process_attestation( @@ -33,7 +32,7 @@ suite "Vote Processing - Slot Tracking": backend.votes[5].next_root == block_root1 backend.votes[5].payload_present == false - # Try to update with OLDER slot - should NOT update + # Try to update with older slot - should NOT update let block_root2 = Eth2Digest.fromHex("0x5555555555555555555555555555555555555555555555555555555555555555") backend.process_attestation( validator_idx, block_root2, Epoch(3), Slot(99), true, cfg) @@ -42,7 +41,7 @@ suite "Vote Processing - Slot Tracking": backend.votes[5].next_slot == Slot(100) backend.votes[5].next_root == block_root1 - # Update with NEWER slot - should update + # Update with newer slot - should update let block_root3 = Eth2Digest.fromHex("0x6666666666666666666666666666666666666666666666666666666666666666") backend.process_attestation( validator_idx, block_root3, Epoch(3), Slot(101), true, cfg) @@ -65,7 +64,7 @@ suite "Vote Processing - Slot Tracking": # Vote preferring EMPTY branch (attestation.data.index = 0) backend.process_attestation( - validator_idx, + validator_idx, Eth2Digest.fromHex("0x7777777777777777777777777777777777777777777777777777777777777777"), Epoch(5), Slot(150), false, cfg) @@ -80,3 +79,84 @@ suite "Vote Processing - Slot Tracking": check: backend.votes[3].payload_present == true + + suite "PTC Voting - Payload Timeliness": + test "is_payload_timely: False when payload not locally available": + var backend = ForkChoiceBackend() + let block_root = Eth2Digest.fromHex( + "0x4444444444444444444444444444444444444444444444444444444444444444") + + backend.ptc_vote[block_root] = newSeq[bool](PTC_SIZE) + for i in 0..<300: + backend.ptc_vote[block_root][i] = true + + # Don't mark as locally available + # (on_execution_payload was not called) + check: + not backend.is_payload_timely(block_root) + + test "is_payload_timely: False with exactly 256 votes (boundary)": + var backend = ForkChoiceBackend() + let block_root = Eth2Digest.fromHex( + "0x7777777777777777777777777777777777777777777777777777777777777777") + + # Exactly 256 votes (at threshold, not above) + backend.ptc_vote[block_root] = newSeq[bool](PTC_SIZE) + for i in 0..<256: + backend.ptc_vote[block_root][i] = true + + backend.execution_payload_states[block_root] = Eth2Digest.fromHex( + "0x8888888888888888888888888888888888888888888888888888888888888888") + + check: + not backend.is_payload_timely(block_root) + + test "is_payload_timely: True with 257 votes (just above threshold)": + var backend = ForkChoiceBackend() + let block_root = Eth2Digest.fromHex( + "0x9999999999999999999999999999999999999999999999999999999999999999") + + # 257 votes (just above threshold) + backend.ptc_vote[block_root] = newSeq[bool](PTC_SIZE) + for i in 0..<257: + backend.ptc_vote[block_root][i] = true + + backend.execution_payload_states[block_root] = Eth2Digest.fromHex( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + check: + backend.is_payload_timely(block_root) + + test "is_payload_timely: Both conditions must be met": + var backend = ForkChoiceBackend() + + # Case 1: Votes YES, Local NO → FALSE + let root1 = Eth2Digest.fromHex( + "0x1111111111111111111111111111111111111111111111111111111111111111") + backend.ptc_vote[root1] = newSeq[bool](PTC_SIZE) + for i in 0..<300: + backend.ptc_vote[root1][i] = true + # Don't add to execution_payload_states + check: + not backend.is_payload_timely(root1) + + # Case 2: Votes NO, Local YES → FALSE + let root2 = Eth2Digest.fromHex( + "0x2222222222222222222222222222222222222222222222222222222222222222") + backend.ptc_vote[root2] = newSeq[bool](PTC_SIZE) + for i in 0..<100: + backend.ptc_vote[root2][i] = true + backend.execution_payload_states[root2] = Eth2Digest.fromHex( + "0x3333333333333333333333333333333333333333333333333333333333333333") + check: + not backend.is_payload_timely(root2) + + # Case 3: Votes YES, Local YES → TRUE + let root3 = Eth2Digest.fromHex( + "0x4444444444444444444444444444444444444444444444444444444444444444") + backend.ptc_vote[root3] = newSeq[bool](PTC_SIZE) + for i in 0..<300: + backend.ptc_vote[root3][i] = true + backend.execution_payload_states[root3] = Eth2Digest.fromHex( + "0x5555555555555555555555555555555555555555555555555555555555555555") + check: + backend.is_payload_timely(root3) From c748e5202ab191e8f26ef258f9ba0be86bba4fd4 Mon Sep 17 00:00:00 2001 From: Caleb Date: Fri, 19 Dec 2025 12:28:02 +0000 Subject: [PATCH 4/4] implement payload extension logic --- beacon_chain/fork_choice/fork_choice.nim | 108 ++++++++++++++++--- tests/test_fork_choice_gloas.nim | 129 ++++++++++++++++++++++- 2 files changed, 223 insertions(+), 14 deletions(-) diff --git a/beacon_chain/fork_choice/fork_choice.nim b/beacon_chain/fork_choice/fork_choice.nim index 9a5d968c38..10f8eaec8c 100644 --- a/beacon_chain/fork_choice/fork_choice.nim +++ b/beacon_chain/fork_choice/fork_choice.nim @@ -202,7 +202,7 @@ proc process_attestation_queue( if it.slot < slot: for validator_index in it.attesting_indices: self.backend.process_attestation( - validator_index, it.block_root, it.slot.epoch(), it.slot, + validator_index, it.block_root, it.slot.epoch(), it.slot, it.committee_index == 1, dag.cfg) false else: @@ -507,7 +507,7 @@ func is_payload_timely*(self: ForkChoiceBackend, root: Eth2Digest): bool = # The beacon block root must be known if root notin self.ptc_vote: return false - + # If the payload is not locally available, the payload # is not considered available regardless of the PTC vote if root notin self.execution_payload_states: @@ -536,21 +536,16 @@ proc on_payload_attestation_message*( slot: Slot, payload_present: bool, is_from_block: bool = false): FcResult[void] = - ## Run ``on_payload_attestation_message`` upon receiving a new ``ptc_message`` directly on the wire. - ## - ## This is called when we receive a PayloadAttestationMessage either: - ## - From gossip (is_from_block = false) - ## - From a block's body.payload_attestations (is_from_block = true) - ## - ## Block at slot N -> PTC votes during slot N -> votes ncluded in slot N+1 - + ## Run ``on_payload_attestation_message`` upon receiving + ## a new ``ptc_message`` directly on the wire. + if not dag.isGloasEnabled(slot): return ok() # The beacon block root must be known if beacon_block_root notin self.backend.proto_array.indices: return ok() - + # PTC attestation must be for a known block. If block is unknown, delay consideration until the block is found if beacon_block_root notin self.backend.ptc_vote: self.backend.ptc_vote[beacon_block_root] = newSeq[bool](PTC_SIZE) @@ -597,7 +592,7 @@ proc on_execution_payload( beacon_block_root: Eth2Digest, execution_payload_state_root: Eth2Digest): FcResult[void] = ## Run ``on_execution_payload`` upon receiving a new execution payload. - + let current_slot = self.checkpoints.time.slotOrZero(dag.timeParams) if not dag.isGloasEnabled(current_slot): return ok() @@ -616,6 +611,95 @@ proc on_execution_payload( state_root = shortLog(execution_payload_state_root) ok() +func should_extend_payload*( + self: var Forkchoice, root: Eth2Digest): bool = + # Slot N: Block B (PENDING) produced + # Payload commitment in block + # Builder should reveal payload + + # Slot N+1: Fork choice deciding: + # Should we extend EMPTY or FULL branch of Block B? + + # Case 1: if payload is timely + if self.backend.is_payload_timely(root): + trace "Extending payload: timely according to PTC", + root = shortLog(root) + return true + + # Case 2: No proposer boost for block + # Optimistic, default to full and assume payload will arrive + let proposer_root = self.checkpoints.proposer_boost_root + if proposer_root.isZero: + trace "Extending payload: no proposer boost", + root = shortLog(root) + return true + + # Does proposer boost conflict with this block? + let proposer_idx = self.backend.proto_array.indices.getOrDefault( + proposer_root, -1) + if proposer_idx < 0: + trace "Extending payload: proposer boost block not found", + root = shortLog(root) + return true + + # Use the proto_array's node accessor + let proposer_node = self.backend.proto_array.nodes.buf[proposer_idx] + + # Check if parent exists + if proposer_node.parent.isNone: + trace "Extending payload: proposer boost at genesis", + root = shortLog(root) + return true + + let + parent_idx = proposer_node.parent.get() + parent_node = self.backend.proto_array.nodes.buf[parent_idx] + + parent_root = parent_node.bid.root + + # Case 3: proposer boost is on a different chain than `root` + if parent_root != root: + trace "Extending payload: proposer boost on different chain", + root = shortLog(root), + proposer_boost_parent = shortLog(parent_root) + return true + + # Case 4: Proposer boost on our chain (conservative approach) + trace "Not extending payload: proposer boost on our chain", + root = shortLog(root), + proposer_boost_root = shortLog(proposer_root) + false + +func get_payload_status_tiebreaker*( + self: var ForkChoice, node: ForkChoiceNode, + current_slot: Slot, dag: ChainDAGRef): uint8 = + if not dag.isGloasEnabled(current_slot): + return node.payload_status + + let node_idx = self.backend.proto_array.indices.getOrDefault(node.root, -1) + if node_idx < 0: + return node.payload_status + + let + proto_node = self.backend.proto_array.nodes.buf[node_idx] + + # Are we deciding on previous slot's payload + is_deciding_on_previous = (proto_node.bid.slot + 1 == current_slot) + + if node.payloadStatus == PAYLOAD_STATUS_PENDING or not is_deciding_on_previous: + return node.payload_status + + # Deciding on previous slot's payload + if node.payloadStatus == PAYLOAD_STATUS_EMPTY: + return 1'u8 + elif node.payloadStatus == PAYLOAD_STATUS_FULL: + if self.should_extend_payload(node.root): + return 2'u8 + else: + return 0'u8 + else: + return 0'u8 # We shouldn't get here ideally + # Sanity checks # ---------------------------------------------------------------------- # Sanity checks on internal private procedures diff --git a/tests/test_fork_choice_gloas.nim b/tests/test_fork_choice_gloas.nim index 09a4ea55e3..f152d05d45 100644 --- a/tests/test_fork_choice_gloas.nim +++ b/tests/test_fork_choice_gloas.nim @@ -10,8 +10,15 @@ import unittest2, - ../beacon_chain/spec/datatypes/base, - ".."/beacon_chain/fork_choice/[fork_choice_types, fork_choice] + chronicles, + ../beacon_chain/spec/[beaconstate, helpers, state_transition], + ../beacon_chain/spec/datatypes/[base, phase0, gloas], + ../beacon_chain/consensus_object_pools/[blockchain_dag], + ../beacon_chain/fork_choice/[fork_choice_types, fork_choice], + ../beacon_chain/validators/validator_monitor, + ../beacon_chain/beacon_chain_db, + ../beacon_chain/gossip_processing/batch_validation, + ./testutil, ./testdbutil suite "Vote Processing - Slot Tracking": test "Gloas: Votes update based on slot": @@ -160,3 +167,121 @@ suite "Vote Processing - Slot Tracking": "0x5555555555555555555555555555555555555555555555555555555555555555") check: backend.is_payload_timely(root3) + +suite "Gloas Payload Extension": + + setup: + var cfg = defaultRuntimeConfig + cfg.ALTAIR_FORK_EPOCH = Epoch(0) + cfg.BELLATRIX_FORK_EPOCH = Epoch(0) + cfg.CAPELLA_FORK_EPOCH = Epoch(0) + cfg.DENEB_FORK_EPOCH = Epoch(0) + cfg.ELECTRA_FORK_EPOCH = Epoch(0) + cfg.FULU_FORK_EPOCH = Epoch(0) + cfg.GLOAS_FORK_EPOCH = Epoch(0) + + var + validatorMonitor = newClone(ValidatorMonitor.init(cfg)) + dag = ChainDAGRef.init( + cfg, cfg.makeTestDB(SLOTS_PER_EPOCH * 3), validatorMonitor, {}) + forkChoice = newClone(ForkChoice.init( + dag.getFinalizedEpochRef(), dag.finalizedHead.blck)) + + test "should_extend_payload: True when payload is timely": + let block_root = Eth2Digest.fromHex( + "0x1111111111111111111111111111111111111111111111111111111111111111") + + # Set up PTC votes + forkChoice[].backend.ptc_vote[block_root] = newSeq[bool](PTC_SIZE) + for i in 0..<300: + forkChoice[].backend.ptc_vote[block_root][i] = true + + # Mark payload as locally available + forkChoice[].backend.execution_payload_states[block_root] = Eth2Digest.fromHex( + "0x2222222222222222222222222222222222222222222222222222222222222222") + + check: + forkChoice[].should_extend_payload(block_root) + + test "should_extend_payload: True when no proposer boost": + let block_root = Eth2Digest.fromHex( + "0x3333333333333333333333333333333333333333333333333333333333333333") + + # Payload not timely + forkChoice[].backend.ptc_vote[block_root] = newSeq[bool](PTC_SIZE) + for i in 0..<100: + forkChoice[].backend.ptc_vote[block_root][i] = true + + # No proposer boost + forkChoice[].checkpoints.proposer_boost_root = ZERO_HASH + + check: + forkChoice[].should_extend_payload(block_root) + + test "should_extend_payload: True when proposer boost on different chain": + let + genesis_root = dag.genesis.get().root + blockB_root = Eth2Digest.fromHex( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + blockC_root = Eth2Digest.fromHex( + "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc") + + # Add blocks to proto_array + discard forkChoice[].backend.process_block( + BlockId(root: blockB_root, slot: Slot(1)), + genesis_root, + FinalityCheckpoints( + justified: Checkpoint(root: genesis_root, epoch: Epoch(0)), + finalized: Checkpoint(root: genesis_root, epoch: Epoch(0)))) + + discard forkChoice[].backend.process_block( + BlockId(root: blockC_root, slot: Slot(1)), + genesis_root, + FinalityCheckpoints( + justified: Checkpoint(root: genesis_root, epoch: Epoch(0)), + finalized: Checkpoint(root: genesis_root, epoch: Epoch(0)))) + + # Payload for B NOT timely + forkChoice[].backend.ptc_vote[blockB_root] = newSeq[bool](PTC_SIZE) + for i in 0..<100: + forkChoice[].backend.ptc_vote[blockB_root][i] = true + + # Block C gets proposer boost (different chain) + forkChoice[].checkpoints.proposer_boost_root = blockC_root + + check: + forkChoice[].should_extend_payload(blockB_root) + + test "should_extend_payload: False when proposer boost on same chain": + let + genesis_root = dag.genesis.get().root + blockB_root = Eth2Digest.fromHex( + "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + blockD_root = Eth2Digest.fromHex( + "0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd") + + # Add blocks to proto_array + discard forkChoice[].backend.process_block( + BlockId(root: blockB_root, slot: Slot(1)), + genesis_root, + FinalityCheckpoints( + justified: Checkpoint(root: genesis_root, epoch: Epoch(0)), + finalized: Checkpoint(root: genesis_root, epoch: Epoch(0)))) + + discard forkChoice[].backend.process_block( + BlockId(root: blockD_root, slot: Slot(2)), # Builds on B + blockB_root, + FinalityCheckpoints( + justified: Checkpoint(root: genesis_root, epoch: Epoch(0)), + finalized: Checkpoint(root: genesis_root, epoch: Epoch(0)))) + + # Payload for B not timely + forkChoice[].backend.ptc_vote[blockB_root] = newSeq[bool](PTC_SIZE) + for i in 0..<100: + forkChoice[].backend.ptc_vote[blockB_root][i] = true + + # Block D gets proposer boost (on same chain as B) + forkChoice[].checkpoints.proposer_boost_root = blockD_root + + check: + not forkChoice[].should_extend_payload(blockB_root)