Merge branch 'master' of github.com:status-im/nim-beacon-chain
This commit is contained in:
commit
9ff1eb4ac8
|
@ -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)
|
|
@ -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) =
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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..
|
||||
|
|
Loading…
Reference in New Issue