Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 239 additions & 11 deletions beacon_chain/fork_choice/fork_choice.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ----------------------------------------------------------------------

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand All @@ -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,
Expand All @@ -230,15 +255,17 @@ 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.
# Delay consideration in the fork choice until their slot is in the past.
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -472,6 +499,207 @@ 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.

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()

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
Expand Down
31 changes: 29 additions & 2 deletions beacon_chain/fork_choice/fork_choice_types.nim
Original file line number Diff line number Diff line change
@@ -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).
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading