Skip to content

Commit

Permalink
implement fork choice
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
arnetheduck committed Mar 13, 2019
1 parent 1479bae commit 66f9524
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 242 deletions.
31 changes: 25 additions & 6 deletions beacon_chain/attestation_pool.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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) =
Expand All @@ -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():
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
26 changes: 12 additions & 14 deletions beacon_chain/beacon_chain_db.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) =
Expand Down
112 changes: 31 additions & 81 deletions beacon_chain/beacon_node.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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))
Expand Down Expand Up @@ -277,43 +243,42 @@ 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))

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,
shard: BEACON_CHAIN_SHARD_NUMBER,
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...
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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!
Expand Down
29 changes: 27 additions & 2 deletions beacon_chain/beacon_node_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
Loading

0 comments on commit 66f9524

Please sign in to comment.