avoid quadratic attestation queue iteration (#5288)

every attestation is processed with a new wall time so we end up
iterating over all attestations for every attestation we queue - this is
4% of cpu time on a subscribe-all-subnets node

* remove redundant zero checks - block root must be an existing block
and therefore cannot be zero
* simplify "hasn't-voted" check to root only (isZeroMemory is dubiously
implemented for objects)
This commit is contained in:
Jacek Sieka 2023-08-14 15:48:30 +02:00 committed by GitHub
parent d171303133
commit f77548310f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 29 additions and 29 deletions

View File

@ -11,7 +11,7 @@ import
# Standard library
std/[sequtils, tables],
# Status libraries
stew/[objects, results], chronicles,
stew/[results], chronicles,
# Internal
../spec/[beaconstate, helpers, state_transition_block],
../spec/datatypes/[phase0, altair, bellatrix],
@ -150,38 +150,17 @@ proc on_tick(
ok()
func process_attestation_queue(self: var ForkChoice) {.gcsafe.}
proc update_time*(self: var ForkChoice, dag: ChainDAGRef, time: BeaconTime):
FcResult[void] =
const step_size = seconds(SECONDS_PER_SLOT.int)
if time > self.checkpoints.time:
# Call on_tick at least once per slot.
while time >= self.checkpoints.time + step_size:
? self.on_tick(dag, self.checkpoints.time + step_size)
if time > self.checkpoints.time:
# Might create two ticks for the last slot.
? self.on_tick(dag, time)
self.process_attestation_queue() # Only run if time changed!
ok()
func process_attestation*(
func process_attestation(
self: var ForkChoiceBackend,
validator_index: ValidatorIndex,
block_root: Eth2Digest,
target_epoch: Epoch
) =
if block_root.isZero:
return
## 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.isZeroMemory:
if target_epoch > vote.next_epoch or vote.next_root.isZero:
vote.next_root = block_root
vote.next_epoch = target_epoch
@ -189,9 +168,12 @@ func process_attestation*(
validator_index = validator_index,
new_vote = shortLog(vote)
func process_attestation_queue(self: var ForkChoice) =
func process_attestation_queue(self: var ForkChoice, slot: Slot) =
# 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.keepItIf:
if it.slot < self.checkpoints.time.slotOrZero:
if it.slot < slot:
for validator_index in it.attesting_indices:
self.backend.process_attestation(
validator_index, it.block_root, it.slot.epoch())
@ -206,6 +188,27 @@ func contains*(self: ForkChoiceBackend, block_root: Eth2Digest): bool =
## In particular, before adding a block, its parent must be known to the fork choice
self.proto_array.indices.contains(block_root)
proc update_time*(self: var ForkChoice, dag: ChainDAGRef, time: BeaconTime):
FcResult[void] =
# `time` is the wall time, meaning it changes on every call typically
const step_size = seconds(SECONDS_PER_SLOT.int)
if time > self.checkpoints.time:
let
preSlot = self.checkpoints.time.slotOrZero()
postSlot = time.slotOrZero()
# Call on_tick at least once per slot.
while time >= self.checkpoints.time + step_size:
? self.on_tick(dag, self.checkpoints.time + step_size)
if time > self.checkpoints.time:
# Might create two ticks for the last slot.
? self.on_tick(dag, time)
if preSlot != postSlot:
self.process_attestation_queue(postSlot)
ok()
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.1/specs/phase0/fork-choice.md#on_attestation
proc on_attestation*(
self: var ForkChoice,
@ -217,9 +220,6 @@ proc on_attestation*(
): FcResult[void] =
? self.update_time(dag, max(wallTime, attestation_slot.start_beacon_time))
if beacon_block_root.isZero:
return ok()
if attestation_slot < self.checkpoints.time.slotOrZero:
for validator_index in attesting_indices:
# attestation_slot and target epoch must match, per attestation rules