Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integrate fork choice #175

Merged
merged 1 commit into from
Mar 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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