nimbus-eth2/beacon_chain/fork_choice.nim

126 lines
5.8 KiB
Nim

import
deques, options, sequtils, tables,
chronicles,
./spec/[beaconstate, datatypes, crypto, digest, helpers, validator], extras,
./attestation_pool, ./beacon_chain_db, ./ssz
# ##################################################################
# Specs
#
# The beacon chain fork choice rule is a hybrid that combines justification and finality with Latest Message Driven (LMD) Greediest Heaviest Observed SubTree (GHOST). At any point in time a [validator](#dfn-validator) `v` subjectively calculates the beacon chain head as follows.
#
# * Let `store` be the set of attestations and blocks
# that the validator `v` has observed and verified
# (in particular, block ancestors must be recursively verified).
# Attestations not part of any chain are still included in `store`.
# * Let `finalized_head` be the finalized block with the highest slot number.
# (A block `B` is finalized if there is a descendant of `B` in `store`
# the processing of which sets `B` as finalized.)
# * Let `justified_head` be the descendant of `finalized_head`
# with the highest slot number that has been justified
# for at least `SLOTS_PER_EPOCH` slots.
# (A block `B` is justified if there is a descendant of `B` in `store`
# the processing of which sets `B` as justified.)
# If no such descendant exists set `justified_head` to `finalized_head`.
# * Let `get_ancestor(store, block, slot)` be the ancestor of `block` with slot number `slot`.
# The `get_ancestor` function can be defined recursively
#
# def get_ancestor(store, block, slot):
# return block if block.slot == slot
# else get_ancestor(store, store.get_parent(block), slot)`.
#
# * Let `get_latest_attestation(store, validator)`
# be the attestation with the highest slot number in `store` from `validator`.
# If several such attestations exist,
# use the one the validator `v` observed first.
# * Let `get_latest_attestation_target(store, validator)`
# be the target block in the attestation `get_latest_attestation(store, validator)`.
# * The head is `lmd_ghost(store, justified_head)`. (See specs)
#
# Departing from specs:
# - We use a simple fork choice rule without finalized and justified head
# - We don't implement "get_latest_attestation(store, validator) -> Attestation"
# nor get_latest_attestation_target
# - We use block hashes (Eth2Digest) instead of raw blocks where possible
proc get_ancestor(
store: BeaconChainDB, blck: Eth2Digest, slot: Slot): Eth2Digest =
## Find the ancestor with a specific slot number
let blk = store.getBlock(blck).get()
if blk.slot == slot:
blck
else:
store.get_ancestor(blk.parent_root, slot) # TODO: Eliminate recursion
# TODO: what if the slot was never observed/verified?
func getVoteCount(aggregation_bitfield: openarray[byte]): int =
## Get the number of votes
# TODO: A bitfield type that tracks that information
# https://github.com/status-im/nim-beacon-chain/issues/19
for validatorIdx in 0 ..< aggregation_bitfield.len * 8:
result += int aggregation_bitfield.get_bitfield_bit(validatorIdx)
func getAttestationVoteCount(
pool: AttestationPool, current_slot: Slot): CountTable[Eth2Digest] =
## Returns all blocks more recent that the current slot
## that were attested and their vote count
# This replaces:
# - get_latest_attestation,
# - get_latest_attestation_targets
# that are used in lmd_ghost for
# ```
# attestation_targets = [get_latest_attestation_target(store, validator)
# for validator in active_validators]
# ```
# Note that attestation_targets in the Eth2 specs can have duplicates
# while the following implementation will count such blockhash multiple times instead.
result = initCountTable[Eth2Digest]()
# TODO iteration API that hides the startingSlot logic?
for slot in current_slot - pool.startingSlot ..< pool.slots.len.uint64:
for attestation in pool.slots[slot].attestations:
for validation in attestation.validations:
# Increase the block attestation counts by the number of validators aggregated
let voteCount = validation.aggregation_bitfield.getVoteCount()
result.inc(attestation.data.beacon_block_root, voteCount)
proc lmdGhost*(
store: BeaconChainDB,
pool: AttestationPool,
state: BeaconState,
blocksChildren: Table[Eth2Digest, seq[Eth2Digest]]): BeaconBlock =
# Recompute the new head of the beacon chain according to
# LMD GHOST (Latest Message Driven - Greediest Heaviest Observed SubTree)
# Raw vote count from all attestations
let rawVoteCount = pool.getAttestationVoteCount(state.slot)
# The real vote count for a block also takes into account votes for its children
# TODO: a Fenwick Tree datastructure to keep track of cumulated votes
# in O(log N) complexity
# https://en.wikipedia.org/wiki/Fenwick_tree
# Nim implementation for cumulative frequencies at
# https://github.com/numforge/laser/blob/990e59fffe50779cdef33aa0b8f22da19e1eb328/benchmarks/random_sampling/fenwicktree.nim
var head = state.latest_block_roots[state.slot mod LATEST_BLOCK_ROOTS_LENGTH]
var childVotes = initCountTable[Eth2Digest]()
while true: # TODO use a O(log N) implementation instead of O(N^2)
let children = blocksChildren[head]
if children.len == 0:
return store.getBlock(head).get()
# For now we assume that all children are direct descendant of the current head
let next_slot = store.getBlock(head).get().slot + 1
for child in children:
doAssert store.getBlock(child).get().slot == next_slot
childVotes.clear()
for target, votes in rawVoteCount.pairs:
if store.getBlock(target).get().slot >= next_slot:
childVotes.inc(store.get_ancestor(target, next_slot), votes)
head = childVotes.largest().key