Merge branch 'master' of github.com:status-im/nim-beacon-chain

This commit is contained in:
Ștefan Talpalaru 2019-03-14 00:05:12 +01:00
commit 9ff1eb4ac8
No known key found for this signature in database
GPG Key ID: CBF7934204F1B6F9
7 changed files with 309 additions and 238 deletions

View File

@ -5,12 +5,12 @@ import
./beacon_chain_db, ./ssz, ./block_pool,
beacon_node_types
proc init*(T: type AttestationPool, blockPool: BlockPool): T =
T(
slots: initDeque[SlotData](),
blockPool: blockPool,
unresolved: initTable[Eth2Digest, UnresolvedAttestation]()
unresolved: initTable[Eth2Digest, UnresolvedAttestation](),
latestAttestations: initTable[ValidatorPubKey, BlockRef]()
)
proc overlaps(a, b: seq[byte]): bool =
@ -183,6 +183,16 @@ proc slotIndex(
int(attestationSlot - pool.startingSlot)
proc updateLatestVotes(
pool: var AttestationPool, state: BeaconState, attestationSlot: Slot,
participants: seq[ValidatorIndex], blck: BlockRef) =
for validator in participants:
let
pubKey = state.validator_registry[validator].pubkey
current = pool.latestAttestations.getOrDefault(pubKey)
if current.isNil or current.slot < attestationSlot:
pool.latestAttestations[pubKey] = blck
proc add*(pool: var AttestationPool,
state: BeaconState,
attestation: Attestation) =
@ -192,13 +202,15 @@ proc add*(pool: var AttestationPool,
# TODO inefficient data structures..
let
attestationSlot = attestation.data.slot
idx = pool.slotIndex(state, attestationSlot.Slot)
attestationSlot = attestation.data.slot.Slot
idx = pool.slotIndex(state, attestationSlot)
slotData = addr pool.slots[idx]
validation = Validation(
aggregation_bitfield: attestation.aggregation_bitfield,
custody_bitfield: attestation.custody_bitfield,
aggregate_signature: attestation.aggregate_signature)
participants = get_attestation_participants(
state, attestation.data, validation.aggregation_bitfield)
var found = false
for a in slotData.attestations.mitems():
@ -215,13 +227,14 @@ proc add*(pool: var AttestationPool,
debug "Ignoring overlapping attestation",
existingParticipants = get_attestation_participants(
state, a.data, v.aggregation_bitfield),
newParticipants = get_attestation_participants(
state, a.data, validation.aggregation_bitfield)
newParticipants = participants
found = true
break
if not found:
a.validations.add(validation)
pool.updateLatestVotes(state, attestationSlot, participants, a.blck)
info "Attestation resolved",
slot = humaneSlotNum(attestation.data.slot),
shard = attestation.data.shard,
@ -243,6 +256,8 @@ proc add*(pool: var AttestationPool,
blck: blck,
validations: @[validation]
))
pool.updateLatestVotes(state, attestationSlot, participants, blck)
info "Attestation resolved",
slot = humaneSlotNum(attestation.data.slot),
shard = attestation.data.shard,
@ -332,3 +347,7 @@ proc resolve*(pool: var AttestationPool, state: BeaconState) =
for a in resolved:
pool.add(state, a)
proc latestAttestation*(
pool: AttestationPool, pubKey: ValidatorPubKey): BlockRef =
pool.latestAttestations.getOrDefault(pubKey)

View File

@ -6,13 +6,20 @@ import
type
BeaconChainDB* = ref object
## Database storing resolved blocks and states - resolved blocks are such
## blocks that form a chain back to the tail block.
backend: TrieDatabaseRef
DbKeyKind = enum
kHashToState
kHashToBlock
kHeadBlock # Pointer to the most recent block seen
kTailBlock # Pointer to the earliest finalized block
kHeadBlock # Pointer to the most recent block selected by the fork choice
kTailBlock ##\
## Pointer to the earliest finalized block - this is the genesis block when
## the chain starts, but might advance as the database gets pruned
## TODO: determine how aggressively the database should be pruned. For a
## healthy network sync, we probably need to store blocks at least
## past the weak subjectivity period.
func subkey(kind: DbKeyKind): array[1, byte] =
result[0] = byte ord(kind)
@ -43,18 +50,9 @@ proc putHead*(db: BeaconChainDB, key: Eth2Digest) =
db.backend.put(subkey(kHeadBlock), key.data) # TODO head block?
proc putState*(db: BeaconChainDB, key: Eth2Digest, value: BeaconState) =
# TODO: prune old states
# TODO: it might be necessary to introduce the concept of a "last finalized
# state" to the storage, so that clients with limited storage have
# a natural state to start recovering from. One idea is to keep a
# special pointer to the state that has ben finalized, and prune all
# other states.
# One issue is that what will become a finalized is revealed only
# long after that state has passed, meaning that we need to keep
# a history of "finalized state candidates" or possibly replay from
# the previous finalized state, if we have that stored. To consider
# here is that the gap between finalized and present state might be
# significant (days), meaning replay might be expensive.
# TODO prune old states - this is less easy than it seems as we never know
# when or if a particular state will become finalized.
db.backend.put(subkey(type value, key), SSZ.encode(value))
proc putState*(db: BeaconChainDB, value: BeaconState) =

View File

@ -3,10 +3,9 @@ import
chronos, chronicles, confutils,
spec/[datatypes, digest, crypto, beaconstate, helpers, validator], conf, time,
state_transition, fork_choice, ssz, beacon_chain_db, validator_pool, extras,
attestation_pool, block_pool, eth2_network,
attestation_pool, block_pool, eth2_network, beacon_node_types,
mainchain_monitor, trusted_state_snapshots,
eth/trie/db, eth/trie/backends/rocksdb_backend,
beacon_node_types
eth/trie/db, eth/trie/backends/rocksdb_backend
const
topicBeaconBlocks = "ethereum/2.1/beacon_chain/blocks"
@ -168,47 +167,16 @@ proc getAttachedValidator(node: BeaconNode, idx: int): AttachedValidator =
let validatorKey = node.state.data.validator_registry[idx].pubkey
return node.attachedValidators.getValidator(validatorKey)
proc updateHead(node: BeaconNode) =
# TODO placeholder logic for running the fork choice
var
head = node.state.blck
headSlot = node.state.data.slot
proc updateHead(node: BeaconNode): BlockRef =
# TODO move all of this logic to BlockPool
let
justifiedHead = node.blockPool.latestJustifiedBlock()
# LRB fork choice - latest resolved block :)
for ph in node.potentialHeads:
let blck = node.blockPool.get(ph)
if blck.isNone():
continue
if blck.get().data.slot >= headSlot:
head = blck.get().refs
headSlot = blck.get().data.slot
node.potentialHeads.setLen(0)
node.blockPool.updateState(node.state, justifiedHead)
if head.root == node.state.blck.root:
debug "No new head found",
stateRoot = shortLog(node.state.root),
blockRoot = shortLog(node.state.blck.root),
stateSlot = humaneSlotNum(node.state.data.slot)
return
node.blockPool.updateState(node.state, head)
# TODO this should probably be in blockpool, but what if updateState is
# called with a non-head block?
node.db.putHeadBlock(node.state.blck.root)
# TODO we should save the state every now and then, but which state do we
# save? When we receive a block and process it, the state from a
# particular epoch may become finalized - but we no longer have it!
# One thing that would work would be to replay from some earlier
# state (the tail?) to the new finalized state, then save that. Another
# option would be to simply save every epoch start state, and eventually
# point it out as it becomes finalized..
info "Updated head",
stateRoot = shortLog(node.state.root),
headBlockRoot = shortLog(node.state.blck.root),
stateSlot = humaneSlotNum(node.state.data.slot)
let newHead = lmdGhost(node.attestationPool, node.state.data, justifiedHead)
node.blockPool.updateHead(node.state, newHead)
newHead
proc makeAttestation(node: BeaconNode,
validator: AttachedValidator,
@ -224,26 +192,24 @@ proc makeAttestation(node: BeaconNode,
# TODO this lazy update of the head is good because it delays head resolution
# until the very latest moment - on the other hand, if it takes long, the
# attestation might be late!
node.updateHead()
let head = node.updateHead()
node.blockPool.updateState(node.state, head)
# Check pending attestations - maybe we found some blocks for them
node.attestationPool.resolve(node.state.data)
# It might be that the latest block we found is an old one - if this is the
# case, we need to fast-forward the state
# TODO maybe this is not necessary? We just use the justified epoch from the
# state - investigate if it can change (and maybe restructure the state
# update code so it becomes obvious... this would require moving away
# from the huge state object)
var state = node.state.data
skipSlots(state, node.state.blck.root, slot)
skipSlots(node.state.data, node.state.blck.root, slot)
# If we call makeAttestation too late, we must advance head only to `slot`
doAssert state.slot == slot,
doAssert node.state.data.slot == slot,
"Corner case: head advanced beyond sheduled attestation slot"
let
attestationData = makeAttestationData(state, shard, node.state.blck.root)
attestationData =
makeAttestationData(node.state.data, shard, node.state.blck.root)
validatorSignature = await validator.signAttestation(attestationData)
var aggregationBitfield = repeat(0'u8, ceil_div8(committeeLen))
@ -277,17 +243,13 @@ proc proposeBlock(node: BeaconNode,
# To propose a block, we should know what the head is, because that's what
# we'll be building the next block upon..
node.updateHead()
let head = node.updateHead()
node.blockPool.updateState(node.state, head)
# To create a block, we'll first apply a partial block to the state, skipping
# some validations.
# TODO technically, we could leave the state with the new block applied here,
# though it works this way as well because eventually we'll receive the
# block through broadcast.. to apply or not to apply permantently, that
# is the question...
var state = node.state.data
skipSlots(state, node.state.blck.root, slot - 1)
skipSlots(node.state.data, node.state.blck.root, slot - 1)
var blockBody = BeaconBlockBody(
attestations: node.attestationPool.getAttestationsForBlock(slot))
@ -295,17 +257,19 @@ proc proposeBlock(node: BeaconNode,
var newBlock = BeaconBlock(
slot: slot,
parent_root: node.state.blck.root,
randao_reveal: validator.genRandaoReveal(state, slot),
randao_reveal: validator.genRandaoReveal(node.state.data, slot),
eth1_data: node.mainchainMonitor.getBeaconBlockRef(),
body: blockBody,
signature: ValidatorSig(), # we need the rest of the block first!
)
let ok =
updateState(state, node.state.blck.root, newBlock, {skipValidation})
updateState(
node.state.data, node.state.blck.root, newBlock, {skipValidation})
doAssert ok # TODO: err, could this fail somehow?
node.state.root = hash_tree_root_final(node.state.data)
newBlock.state_root = Eth2Digest(data: hash_tree_root(state))
newBlock.state_root = node.state.root
let proposal = Proposal(
slot: slot.uint64,
@ -313,7 +277,8 @@ proc proposeBlock(node: BeaconNode,
block_root: Eth2Digest(data: signed_root(newBlock, "signature")),
signature: ValidatorSig(),
)
newBlock.signature = await validator.signBlockProposal(state.fork, proposal)
newBlock.signature =
await validator.signBlockProposal(node.state.data.fork, proposal)
# TODO what are we waiting for here? broadcast should never block, and never
# fail...
@ -396,7 +361,8 @@ proc scheduleEpochActions(node: BeaconNode, epoch: Epoch) =
stateEpoch = humaneEpochNum(node.state.data.slot.slot_to_epoch())
# In case some late blocks dropped in
node.updateHead()
let head = node.updateHead()
node.blockPool.updateState(node.state, head)
# Sanity check - verify that the current head block is not too far behind
if node.state.data.slot.slot_to_epoch() + 1 < epoch:
@ -524,9 +490,6 @@ proc onAttestation(node: BeaconNode, attestation: Attestation) =
node.attestationPool.add(node.state.data, attestation)
if attestation.data.beacon_block_root notin node.potentialHeads:
node.potentialHeads.add attestation.data.beacon_block_root
proc onBeaconBlock(node: BeaconNode, blck: BeaconBlock) =
# We received a block but don't know much about it yet - in particular, we
# don't know if it's part of the chain we're currently building.
@ -544,24 +507,11 @@ proc onBeaconBlock(node: BeaconNode, blck: BeaconBlock) =
voluntary_exits = blck.body.voluntary_exits.len,
transfers = blck.body.transfers.len
var
# TODO We could avoid this copy by having node.state as a general cache
# that just holds a random recent state - that would however require
# rethinking scheduling etc, which relies on there being a fairly
# accurate representation of the state available. Notably, when there's
# a reorg, the scheduling might change!
stateTmp = node.state
if not node.blockPool.add(stateTmp, blockRoot, blck):
if not node.blockPool.add(node.state, blockRoot, blck):
# TODO the fact that add returns a bool that causes the parent block to be
# pre-emptively fetched is quite ugly - fix.
node.fetchBlocks(@[blck.parent_root])
# Delay updating the head until the latest moment possible - this makes it
# more likely that we've managed to resolve the block, in case of
# irregularities
if blockRoot notin node.potentialHeads:
node.potentialHeads.add blockRoot
# The block we received contains attestations, and we might not yet know about
# all of them. Let's add them to the attestation pool - in case they block
# is not yet resolved, neither will the attestations be!

View File

@ -30,7 +30,11 @@ type
keys*: KeyPair
attachedValidators*: ValidatorPool
blockPool*: BlockPool
state*: StateData
state*: StateData ##\
## State cache object that's used as a scratch pad
## TODO this is pretty dangerous - for example if someone sets it
## to a particular state then does `await`, it might change - prone to
## async races
attestationPool*: AttestationPool
mainchainMonitor*: MainchainMonitor
potentialHeads*: seq[Eth2Digest]
@ -98,6 +102,10 @@ type
unresolved*: Table[Eth2Digest, UnresolvedAttestation]
latestAttestations*: Table[ValidatorPubKey, BlockRef] ##\
## Map that keeps track of the most recent vote of each attester - see
## fork_choice
# #############################################
#
# Block Pool
@ -148,9 +156,16 @@ type
blocksBySlot*: Table[uint64, seq[BlockRef]]
tail*: BlockData ##\
tail*: BlockRef ##\
## The earliest finalized block we know about
head*: BlockRef ##\
## The latest block we know about, that's been chosen as a head by the fork
## choice rule
finalizedHead*: BlockRef ##\
## The latest block that was finalized according to the block in head
db*: BeaconChainDB
UnresolvedBlock* = object
@ -169,6 +184,16 @@ type
children*: seq[BlockRef]
slot*: Slot # TODO could calculate this by walking to root, but..
justified*: bool ##\
## True iff there exists a descendant of this block that generates a state
## that points back to this block in its `justified_epoch` field.
finalized*: bool ##\
## True iff there exists a descendant of this block that generates a state
## that points back to this block in its `finalized_epoch` field.
## Ancestors of this block are guaranteed to have 1 child only.
BlockData* = object
## Body and graph in one

View File

@ -1,8 +1,8 @@
import
bitops, chronicles, options, tables,
bitops, chronicles, options, sequtils, tables,
ssz, beacon_chain_db, state_transition, extras,
spec/[crypto, datatypes, digest],
beacon_node_types
beacon_node_types,
spec/[crypto, datatypes, digest, helpers]
proc link(parent, child: BlockRef) =
doAssert (not (parent.root == Eth2Digest() or child.root == Eth2Digest())),
@ -12,10 +12,24 @@ proc link(parent, child: BlockRef) =
child.parent = parent
parent.children.add(child)
proc init*(T: type BlockRef, root: Eth2Digest, slot: Slot): BlockRef =
BlockRef(
root: root,
slot: slot
)
proc init*(T: type BlockRef, root: Eth2Digest, blck: BeaconBlock): BlockRef =
BlockRef.init(root, blck.slot)
proc findAncestorBySlot(blck: BlockRef, slot: Slot): BlockRef =
result = blck
while result != nil and result.slot > slot:
result = result.parent
proc init*(T: type BlockPool, db: BeaconChainDB): BlockPool =
# TODO we require that the db contains both a head and a tail block -
# asserting here doesn't seem like the right way to go about it however..
# TODO head is updated outside of block pool but read here - ugly.
let
tail = db.getTailBlock()
@ -25,46 +39,85 @@ proc init*(T: type BlockPool, db: BeaconChainDB): BlockPool =
doAssert head.isSome(), "Missing head block, database corrupt?"
let
headRoot = head.get()
tailRoot = tail.get()
tailRef = BlockRef(root: tailRoot)
tailBlock = db.getBlock(tailRoot).get()
tailRef = BlockRef.init(tailRoot, tailBlock)
headRoot = head.get()
var blocks = {tailRef.root: tailRef}.toTable()
var
blocks = {tailRef.root: tailRef}.toTable()
latestStateRoot = Option[Eth2Digest]()
headStateBlock = tailRef
headRef: BlockRef
if headRoot != tailRoot:
var curRef: BlockRef
for root, _ in db.getAncestors(headRoot):
for root, blck in db.getAncestors(headRoot):
if root == tailRef.root:
doAssert(not curRef.isNil)
link(tailRef, curRef)
curRef = curRef.parent
break
let newRef = BlockRef.init(root, blck)
if curRef == nil:
curRef = BlockRef(root: root)
curRef = newRef
headRef = newRef
else:
link(BlockRef(root: root), curRef)
link(newRef, curRef)
curRef = curRef.parent
blocks[curRef.root] = curRef
if latestStateRoot.isNone() and db.containsState(blck.state_root):
latestStateRoot = some(blck.state_root)
doAssert curRef == tailRef,
"head block does not lead to tail, database corrupt?"
else:
headRef = tailRef
var blocksBySlot = initTable[uint64, seq[BlockRef]]()
for _, b in tables.pairs(blocks):
let slot = db.getBlock(b.root).get().slot
blocksBySlot.mgetOrPut(slot.uint64, @[]).add(b)
let
# The head state is necessary to find out what we considered to be the
# finalized epoch last time we saved something.
headStateRoot =
if latestStateRoot.isSome():
latestStateRoot.get()
else:
db.getBlock(tailRef.root).get().state_root
# TODO right now, because we save a state at every epoch, this *should*
# be the latest justified state or newer, meaning it's enough for
# establishing what we consider to be the finalized head. This logic
# will need revisiting however
headState = db.getState(headStateRoot).get()
finalizedHead =
headRef.findAncestorBySlot(headState.finalized_epoch.get_epoch_start_slot())
justifiedHead =
headRef.findAncestorBySlot(headState.justified_epoch.get_epoch_start_slot())
doAssert justifiedHead.slot >= finalizedHead.slot,
"justified head comes before finalized head - database corrupt?"
# TODO what about ancestors? only some special blocks are
# finalized / justified but to find out exactly which ones, we would have
# to replay state transitions from tail to head and note each one...
finalizedHead.finalized = true
justifiedHead.justified = true
BlockPool(
pending: initTable[Eth2Digest, BeaconBlock](),
unresolved: initTable[Eth2Digest, UnresolvedBlock](),
blocks: blocks,
blocksBySlot: blocksBySlot,
tail: BlockData(
data: db.getBlock(tailRef.root).get(),
refs: tailRef,
),
tail: tailRef,
head: headRef,
finalizedHead: finalizedHead,
db: db
)
@ -98,13 +151,14 @@ proc add*(
return true
# The tail block points to a cutoff time beyond which we don't store blocks -
# if we receive a block with an earlier slot, there's no hope of ever
# resolving it
if blck.slot <= pool.tail.data.slot:
# If the block we get is older than what we finalized already, we drop it.
# One way this can happen is that we start resolving a block and finalization
# happens in the meantime - the block we requested will then be stale
# by the time it gets here.
if blck.slot <= pool.finalizedHead.slot:
debug "Old block, dropping",
slot = humaneSlotNum(blck.slot),
tailSlot = humaneSlotNum(pool.tail.data.slot),
tailSlot = humaneSlotNum(pool.tail.slot),
stateRoot = shortLog(blck.state_root),
parentRoot = shortLog(blck.parent_root),
blockRoot = shortLog(blockRoot)
@ -139,9 +193,9 @@ proc add*(
voluntary_exits = blck.body.voluntary_exits.len,
transfers = blck.body.transfers.len
let blockRef = BlockRef(
root: blockRoot
)
return
let blockRef = BlockRef.init(blockRoot, blck)
link(parent, blockRef)
pool.blocks[blockRoot] = blockRef
@ -151,6 +205,22 @@ proc add*(
# Resolved blocks should be stored in database
pool.db.putBlock(blockRoot, blck)
# This block *might* have caused a justification - make sure we stow away
# that information:
let
justifiedBlock =
blockRef.findAncestorBySlot(
state.data.justified_epoch.get_epoch_start_slot())
if not justifiedBlock.justified:
info "Justified block",
justifiedBlockRoot = shortLog(justifiedBlock.root),
justifiedBlockRoot = humaneSlotnum(justifiedBlock.slot),
headBlockRoot = shortLog(blockRoot),
headBlockSlot = humaneSlotnum(blck.slot)
justifiedBlock.justified = true
info "Block resolved",
blockRoot = shortLog(blockRoot),
slot = humaneSlotNum(blck.slot),
@ -322,7 +392,7 @@ proc updateState*(
blockRoot = shortLog(blck.root)
doAssert false, "Oh noes, we passed big bang!"
notice "Replaying state transitions",
debug "Replaying state transitions",
stateSlot = humaneSlotNum(state.data.slot),
stateRoot = shortLog(ancestor.data.state_root),
prevStateSlot = humaneSlotNum(ancestorState.get().slot),
@ -358,8 +428,86 @@ proc updateState*(
proc loadTailState*(pool: BlockPool): StateData =
## Load the state associated with the current tail in the pool
let stateRoot = pool.db.getBlock(pool.tail.root).get().state_root
StateData(
data: pool.db.getState(pool.tail.data.state_root).get(),
root: pool.tail.data.state_root,
blck: pool.tail.refs
data: pool.db.getState(stateRoot).get(),
root: stateRoot,
blck: pool.tail
)
proc updateHead*(pool: BlockPool, state: var StateData, blck: BlockRef) =
## Update what we consider to be the current head, as given by the fork
## choice.
## The choice of head affects the choice of finalization point - the order
## of operations naturally becomes important here - after updating the head,
## blocks that were once considered potential candidates for a tree will
## now fall from grace, or no longer be considered resolved.
if pool.head == blck:
debug "No head update this time",
headBlockRoot = shortLog(blck.root),
headBlockSlot = humaneSlotNum(blck.slot)
return
pool.head = blck
# Start off by making sure we have the right state
updateState(pool, state, blck)
info "Updated head",
stateRoot = shortLog(state.root),
headBlockRoot = shortLog(state.blck.root),
stateSlot = humaneSlotNum(state.data.slot)
let
# TODO there might not be a block at the epoch boundary - what then?
finalizedHead =
blck.findAncestorBySlot(state.data.finalized_epoch.get_epoch_start_slot())
doAssert (not finalizedHead.isNil),
"Block graph should always lead to a finalized block"
if finalizedHead != pool.finalizedHead:
info "Finalized block",
finalizedBlockRoot = shortLog(finalizedHead.root),
finalizedBlockSlot = humaneSlotNum(finalizedHead.slot),
headBlockRoot = shortLog(blck.root),
headBlockSlot = humaneSlotNum(blck.slot)
var cur = finalizedHead
while cur != pool.finalizedHead:
# Finalization means that we choose a single chain as the canonical one -
# it also means we're no longer interested in any branches from that chain
# up to the finalization point
# TODO technically, if we remove from children the gc should free the block
# because it should become orphaned, via mark&sweep if nothing else,
# though this needs verification
# TODO what about attestations? we need to drop those too, though they
# *should* be pretty harmless
# TODO remove from database as well.. here, or using some GC-like setup
# that periodically cleans it up?
for child in cur.parent.children:
if child != cur:
pool.blocks.del(child.root)
cur.parent.children = @[cur]
cur = cur.parent
pool.finalizedHead = finalizedHead
proc findLatestJustifiedBlock(
blck: BlockRef, depth: int, deepest: var tuple[depth: int, blck: BlockRef]) =
if blck.justified and depth > deepest.depth:
deepest = (depth, blck)
for child in blck.children:
findLatestJustifiedBlock(child, depth + 1, deepest)
proc latestJustifiedBlock*(pool: BlockPool): BlockRef =
## Return the most recent block that is justified and at least as recent
## as the latest finalized block
var deepest = (0, pool.finalizedHead)
findLatestJustifiedBlock(pool.finalizedHead, 0, deepest)
deepest[1]

View File

@ -2,124 +2,55 @@ import
deques, options, sequtils, tables,
chronicles,
./spec/[beaconstate, datatypes, crypto, digest, helpers, validator], extras,
./beacon_node_types, ./beacon_chain_db, ./ssz
./attestation_pool, ./beacon_node_types, ./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:
proc get_ancestor(blck: BlockRef, slot: Slot): BlockRef =
if blck.slot == slot:
blck
elif blck.slot < slot:
nil
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)
get_ancestor(blck.parent, slot)
# https://github.com/ethereum/eth2.0-specs/blob/v0.4.0/specs/core/0_beacon-chain.md#beacon-chain-fork-choice-rule
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
pool: AttestationPool, start_state: BeaconState,
start_block: BlockRef): BlockRef =
# 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]()
let
active_validator_indices =
get_active_validator_indices(
start_state.validator_registry, slot_to_epoch(start_state.slot))
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()
var attestation_targets: seq[tuple[validator: ValidatorIndex, blck: BlockRef]]
for i in active_validator_indices:
let pubKey = start_state.validator_registry[i].pubkey
if (let vote = pool.latestAttestation(pubKey); not vote.isNil):
attestation_targets.add((i, vote))
# 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
template get_vote_count(blck: BlockRef): uint64 =
var res: uint64
for validator_index, target in attestation_targets.items():
if get_ancestor(target, blck.slot) == blck:
res += get_effective_balance(start_state, validator_index) div
FORK_CHOICE_BALANCE_INCREMENT
res
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)
var head = start_block
while true:
if head.children.len() == 0:
return head
head = childVotes.largest().key
head = head.children[0]
var
headCount = get_vote_count(head)
for i in 1..<head.children.len:
if (let hc = get_vote_count(head.children[i]); hc > headCount):
head = head.children[i]
headCount = hc

View File

@ -16,16 +16,15 @@ suite "Attestation pool processing":
## mock data.
# Genesis state with minimal number of deposits
var
let
genState = get_genesis_beacon_state(
makeInitialDeposits(flags = {skipValidation}), 0, Eth1Data(),
{skipValidation})
genBlock = get_initial_beacon_block(genState)
blockPool = BlockPool.init(makeTestDB(genState, genBlock))
test "Can add and retrieve simple attestation":
var
blockPool = BlockPool.init(makeTestDB(genState, genBlock))
pool = AttestationPool.init(blockPool)
state = blockPool.loadTailState()
# Slot 0 is a finalized slot - won't be making attestations for it..
@ -48,6 +47,7 @@ suite "Attestation pool processing":
test "Attestations may arrive in any order":
var
blockPool = BlockPool.init(makeTestDB(genState, genBlock))
pool = AttestationPool.init(blockPool)
state = blockPool.loadTailState()
# Slot 0 is a finalized slot - won't be making attestations for it..