From 66f952446357bb86aac615f3bd1107ba144d6484 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Tue, 12 Mar 2019 08:43:40 -0600 Subject: [PATCH] implement fork choice * keep track of a finalized block * keep track of all justified blocks * use naive spec version of LMD ghost * cache slot number and a few more things in BlockRef * keep track of the latest vote of each validator * depend less on the state of node.state (it's a cache, effectively) --- beacon_chain/attestation_pool.nim | 31 ++++- beacon_chain/beacon_chain_db.nim | 26 ++-- beacon_chain/beacon_node.nim | 112 +++++----------- beacon_chain/beacon_node_types.nim | 29 ++++- beacon_chain/block_pool.nim | 200 +++++++++++++++++++++++++---- beacon_chain/fork_choice.nim | 151 ++++++---------------- tests/test_attestation_pool.nim | 6 +- 7 files changed, 313 insertions(+), 242 deletions(-) diff --git a/beacon_chain/attestation_pool.nim b/beacon_chain/attestation_pool.nim index 4d5d0a8552..8079ce77d0 100644 --- a/beacon_chain/attestation_pool.nim +++ b/beacon_chain/attestation_pool.nim @@ -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) \ No newline at end of file diff --git a/beacon_chain/beacon_chain_db.nim b/beacon_chain/beacon_chain_db.nim index 5db4f658a3..946bae97ff 100644 --- a/beacon_chain/beacon_chain_db.nim +++ b/beacon_chain/beacon_chain_db.nim @@ -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) = diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index b4e7af6e61..6c14ca1cf3 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -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 - - # 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) - - 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) +proc updateHead(node: BeaconNode): BlockRef = + # TODO move all of this logic to BlockPool + let + justifiedHead = node.blockPool.latestJustifiedBlock() - # 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.. + node.blockPool.updateState(node.state, justifiedHead) - 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! diff --git a/beacon_chain/beacon_node_types.nim b/beacon_chain/beacon_node_types.nim index faed7fcac3..a3148394fb 100644 --- a/beacon_chain/beacon_node_types.nim +++ b/beacon_chain/beacon_node_types.nim @@ -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 diff --git a/beacon_chain/block_pool.nim b/beacon_chain/block_pool.nim index c42a3c11dd..8b1044df5d 100644 --- a/beacon_chain/block_pool.nim +++ b/beacon_chain/block_pool.nim @@ -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: assert(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] diff --git a/beacon_chain/fork_choice.nim b/beacon_chain/fork_choice.nim index ae4623e6be..ab6b72cd10 100644 --- a/beacon_chain/fork_choice.nim +++ b/beacon_chain/fork_choice.nim @@ -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]() - - 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 + let + active_validator_indices = + get_active_validator_indices( + start_state.validator_registry, slot_to_epoch(start_state.slot)) + + 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)) + + 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 + + var head = start_block + while true: + if head.children.len() == 0: + return head + + head = head.children[0] + var + headCount = get_vote_count(head) + + for i in 1.. headCount): + head = head.children[i] + headCount = hc diff --git a/tests/test_attestation_pool.nim b/tests/test_attestation_pool.nim index 929861f324..b77dbc0b02 100644 --- a/tests/test_attestation_pool.nim +++ b/tests/test_attestation_pool.nim @@ -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..