head block selection fixes (#259)

* track justified and head blocks correctly in presence of blank slots
* fix new block state root
* keep list of viable heads instead of walking children
* disable json bitfield deserializer (needs a corresponding serializer)
* fix handshake best & finalized root
This commit is contained in:
Jacek Sieka 2019-05-01 03:19:29 -06:00 committed by Yuriy Glukhov
parent 46b4154ce8
commit bd4893d2e8
5 changed files with 155 additions and 143 deletions

View File

@ -272,7 +272,7 @@ proc updateHead(node: BeaconNode, slot: Slot): BlockRef =
# Use head state for attestation resolution below
# TODO do we need to resolve attestations using all available head states?
node.blockPool.withState(
node.stateCache, BlockSlot(blck: node.blockPool.head, slot: slot)):
node.stateCache, BlockSlot(blck: node.blockPool.head.blck, slot: slot)):
# Check pending attestations - maybe we found some blocks for them
node.attestationPool.resolve(state)
@ -374,7 +374,7 @@ proc proposeBlock(node: BeaconNode,
let ok = updateState(tmpState, newBlock, {skipValidation})
doAssert ok # TODO: err, could this fail somehow?
newBlock.state_root = hash_tree_root(state)
newBlock.state_root = hash_tree_root(tmpState)
let blockRoot = signed_root(newBlock)
@ -413,7 +413,7 @@ proc onAttestation(node: BeaconNode, attestation: Attestation) =
# the attestation for some of the check? Consider interop with block
# production!
node.blockPool.withState(node.stateCache,
BlockSlot(blck: node.blockPool.head, slot: node.beaconClock.now().toSlot())):
BlockSlot(blck: node.blockPool.head.blck, slot: node.beaconClock.now().toSlot())):
node.attestationPool.add(state, attestation)
proc onBeaconBlock(node: BeaconNode, blck: BeaconBlock) =
@ -452,7 +452,7 @@ proc handleAttestations(node: BeaconNode, head: BlockRef, slot: Slot) =
return
let attestationHead = head.findAncestorBySlot(slot)
if head != attestationHead:
if head != attestationHead.blck:
# In rare cases, such as when we're busy syncing or just slow, we'll be
# attesting to a past state - we must then recreate the world as it looked
# like back then
@ -462,7 +462,7 @@ proc handleAttestations(node: BeaconNode, head: BlockRef, slot: Slot) =
attestationSlot = humaneSlotNum(slot)
debug "Checking attestations",
attestationHeadRoot = shortLog(attestationHead.root),
attestationHeadRoot = shortLog(attestationHead.blck.root),
attestationSlot = humaneSlotNum(slot)
@ -474,8 +474,7 @@ proc handleAttestations(node: BeaconNode, head: BlockRef, slot: Slot) =
# We need to run attestations exactly for the slot that we're attesting to.
# In case blocks went missing, this means advancing past the latest block
# using empty slots as fillers.
node.blockPool.withState(
node.stateCache, BlockSlot(blck: attestationHead, slot: slot)):
node.blockPool.withState(node.stateCache, attestationHead):
for crosslink_committee in get_crosslink_committees_at_slot(state, slot):
for i, validatorIdx in crosslink_committee.committee:
let validator = node.getAttachedValidator(state, validatorIdx)

View File

@ -171,15 +171,18 @@ type
tail*: BlockRef ##\
## The earliest finalized block we know about
head*: BlockRef ##\
head*: Head ##\
## The latest block we know about, that's been chosen as a head by the fork
## choice rule
finalizedHead*: BlockRef ##\
finalizedHead*: BlockSlot ##\
## The latest block that was finalized according to the block in head
## Ancestors of this block are guaranteed to have 1 child only.
db*: BeaconChainDB
heads*: seq[Head]
MissingBlock* = object
slots*: uint64 # number of slots that are suspected missing
tries*: int
@ -196,17 +199,10 @@ type
## Not nil, except for the tail
children*: seq[BlockRef]
# TODO do we strictly need this?
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
@ -236,6 +232,10 @@ type
blck*: BlockRef
slot*: Slot
Head* = object
blck*: BlockRef
justified*: BlockSlot
# #############################################
#
# Validator Pool

View File

@ -27,32 +27,32 @@ proc init*(T: type BlockRef, root: Eth2Digest, slot: Slot): BlockRef =
proc init*(T: type BlockRef, root: Eth2Digest, blck: BeaconBlock): BlockRef =
BlockRef.init(root, blck.slot)
proc findAncestorBySlot*(blck: BlockRef, slot: Slot): BlockRef =
proc findAncestorBySlot*(blck: BlockRef, slot: Slot): BlockSlot =
## Find the first ancestor that has a slot number less than or equal to `slot`
assert(not blck.isNil)
result = blck
var ret = blck
while result.parent != nil and result.slot > slot:
result = result.parent
while ret.parent != nil and ret.slot > slot:
ret = ret.parent
assert(not result.isNil)
BlockSlot(blck: ret, slot: slot)
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..
let
tail = db.getTailBlock()
head = db.getHeadBlock()
tailBlockRoot = db.getTailBlock()
headBlockRoot = db.getHeadBlock()
doAssert tail.isSome(), "Missing tail block, database corrupt?"
doAssert head.isSome(), "Missing head block, database corrupt?"
doAssert tailBlockRoot.isSome(), "Missing tail block, database corrupt?"
doAssert headBlockRoot.isSome(), "Missing head block, database corrupt?"
let
tailRoot = tail.get()
tailRoot = tailBlockRoot.get()
tailBlock = db.getBlock(tailRoot).get()
tailRef = BlockRef.init(tailRoot, tailBlock)
headRoot = head.get()
headRoot = headBlockRoot.get()
var
blocks = {tailRef.root: tailRef}.toTable()
@ -108,17 +108,13 @@ proc init*(T: type BlockPool, db: BeaconChainDB): BlockPool =
headState = db.getState(headStateRoot).get()
finalizedHead =
headRef.findAncestorBySlot(headState.finalized_epoch.get_epoch_start_slot())
justifiedHead =
headRef.findAncestorBySlot(headState.current_justified_epoch.get_epoch_start_slot())
justifiedSlot = headState.current_justified_epoch.get_epoch_start_slot()
justifiedHead = headRef.findAncestorBySlot(justifiedSlot)
head = Head(blck: headRef, justified: justifiedHead)
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](),
@ -126,9 +122,10 @@ proc init*(T: type BlockPool, db: BeaconChainDB): BlockPool =
blocks: blocks,
blocksBySlot: blocksBySlot,
tail: tailRef,
head: headRef,
head: head,
finalizedHead: finalizedHead,
db: db
db: db,
heads: @[head]
)
proc addSlotMapping(pool: BlockPool, slot: uint64, br: BlockRef) =
@ -140,6 +137,66 @@ proc addSlotMapping(pool: BlockPool, slot: uint64, br: BlockRef) =
proc updateStateData*(
pool: BlockPool, state: var StateData, bs: BlockSlot) {.gcsafe.}
proc add*(
pool: var BlockPool, state: var StateData, blockRoot: Eth2Digest,
blck: BeaconBlock): BlockRef {.gcsafe.}
proc addResolvedBlock(
pool: var BlockPool, state: var StateData, blockRoot: Eth2Digest,
blck: BeaconBlock, parent: BlockRef): BlockRef =
let blockRef = BlockRef.init(blockRoot, blck)
link(parent, blockRef)
pool.blocks[blockRoot] = blockRef
pool.addSlotMapping(blck.slot.uint64, blockRef)
# Resolved blocks should be stored in database
pool.db.putBlock(blockRoot, blck)
# TODO this is a bit ugly - we update state.data outside of this function then
# set the rest here - need a blockRef to update it. Clean this up -
# hopefully it won't be necessary by the time hash caching and the rest
# is done..
doAssert state.data.slot == blockRef.slot
state.root = blck.state_root
state.blck = blockRef
# This block *might* have caused a justification - make sure we stow away
# that information:
let justifiedSlot = state.data.current_justified_epoch.get_epoch_start_slot()
var foundHead: Option[Head]
for head in pool.heads.mitems():
if head.blck.root == blck.previous_block_root:
if head.justified.slot != justifiedSlot:
head.justified = blockRef.findAncestorBySlot(justifiedSlot)
foundHead = some(head)
break
if foundHead.isNone():
foundHead = some(Head(
blck: blockRef,
justified: blockRef.findAncestorBySlot(justifiedSlot)))
pool.heads.add(foundHead.get())
info "Block resolved",
blck = shortLog(blck),
blockRoot = shortLog(blockRoot),
justifiedRoot = shortLog(foundHead.get().justified.blck.root),
justifiedSlot = humaneSlotNum(foundHead.get().justified.slot)
# Now that we have the new block, we should see if any of the previously
# unresolved blocks magically become resolved
# TODO there are more efficient ways of doing this that don't risk
# running out of stack etc
let retries = pool.pending
for k, v in retries:
discard pool.add(state, k, v)
blockRef
proc add*(
pool: var BlockPool, state: var StateData, blockRoot: Eth2Digest,
blck: BeaconBlock): BlockRef {.gcsafe.} =
@ -171,7 +228,6 @@ proc add*(
return
let parent = pool.blocks.getOrDefault(blck.previous_block_root)
if parent != nil:
@ -194,48 +250,7 @@ proc add*(
return
let blockRef = BlockRef.init(blockRoot, blck)
link(parent, blockRef)
pool.blocks[blockRoot] = blockRef
pool.addSlotMapping(blck.slot.uint64, blockRef)
# Resolved blocks should be stored in database
pool.db.putBlock(blockRoot, blck)
state.root = blck.state_root
state.blck = blockRef
# This block *might* have caused a justification - make sure we stow away
# that information:
let
justifiedBlock =
blockRef.findAncestorBySlot(
state.data.current_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",
blck = shortLog(blck),
blockRoot = shortLog(blockRoot)
# Now that we have the new block, we should see if any of the previously
# unresolved blocks magically become resolved
# TODO there are more efficient ways of doing this, that also don't risk
# running out of stack etc
let retries = pool.pending
for k, v in retries:
discard pool.add(state, k, v)
return blockRef
return pool.addResolvedBlock(state, blockRoot, blck, parent)
pool.pending[blockRoot] = blck
@ -269,8 +284,8 @@ proc add*(
pool.missing[blck.previous_block_root] = MissingBlock(
slots:
# The block is at least two slots ahead - try to grab whole history
if parentSlot > pool.head.slot:
parentSlot - pool.head.slot
if parentSlot > pool.head.blck.slot:
parentSlot - pool.head.blck.slot
else:
# It's a sibling block from a branch that we're missing - fetch one
# epoch at a time
@ -474,6 +489,14 @@ proc loadTailState*(pool: BlockPool): StateData =
blck: pool.tail
)
func isAncestorOf*(a, b: BlockRef): bool =
if a == b:
true
elif a.slot >= b.slot or b.parent.isNil:
false
else:
a.isAncestorOf(b.parent)
proc updateHead*(pool: BlockPool, state: var StateData, blck: BlockRef) =
## Update what we consider to be the current head, as given by the fork
## choice.
@ -481,7 +504,7 @@ proc updateHead*(pool: BlockPool, state: var StateData, blck: BlockRef) =
## 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:
if pool.head.blck == blck:
debug "No head update this time",
headBlockRoot = shortLog(blck.root),
headBlockSlot = humaneSlotNum(blck.slot)
@ -490,15 +513,16 @@ proc updateHead*(pool: BlockPool, state: var StateData, blck: BlockRef) =
let
lastHead = pool.head
pool.head = blck
pool.db.putHeadBlock(blck.root)
# Start off by making sure we have the right state
updateStateData(pool, state, BlockSlot(blck: blck, slot: blck.slot))
let justifiedSlot = state.data.current_justified_epoch.get_epoch_start_slot()
pool.head = Head(blck: blck, justified: blck.findAncestorBySlot(justifiedSlot))
if lastHead != blck.parent:
if lastHead.blck != blck.parent:
notice "Updated head with new parent",
lastHeadRoot = shortLog(lastHead.root),
lastHeadRoot = shortLog(lastHead.blck.root),
parentRoot = shortLog(blck.parent.root),
stateRoot = shortLog(state.root),
headBlockRoot = shortLog(state.blck.root),
@ -518,67 +542,58 @@ proc updateHead*(pool: BlockPool, state: var StateData, blck: BlockRef) =
finalizedHead =
blck.findAncestorBySlot(state.data.finalized_epoch.get_epoch_start_slot())
doAssert (not finalizedHead.isNil),
doAssert (not finalizedHead.blck.isNil),
"Block graph should always lead to a finalized block"
if finalizedHead != pool.finalizedHead:
info "Finalized block",
finalizedBlockRoot = shortLog(finalizedHead.root),
finalizedBlockRoot = shortLog(finalizedHead.blck.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
var cur = finalizedHead.blck
while cur != pool.finalizedHead.blck:
# 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
# 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
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)
let hlen = pool.heads.len
for i in 0..<hlen:
let n = hlen - i - 1
if pool.heads[n].blck.slot < pool.finalizedHead.blck.slot and
not pool.heads[n].blck.isAncestorOf(pool.finalizedHead.blck):
pool.heads.del(n)
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]
proc latestState*(pool: BlockPool): BeaconState =
var b = pool.head
while true:
if b.isNil:
raise newException(Exception, "No state found")
if (let blk = pool.db.getBlock(b.root); blk.isSome()):
if (let state = pool.db.getState(blk.get().stateRoot); state.isSome()):
return state.get()
else:
error "Block from block pool not found in db", root = b.root
b = b.parent
doAssert pool.heads.len > 0,
"We should have at least the genesis block in heaads"
doAssert (not pool.head.blck.isNil()),
"Genesis block will be head, if nothing else"
# Prefer stability: use justified block from current head to break ties!
result = pool.head.justified.blck
for head in pool.heads[1 ..< ^0]:
if head.justified.blck.slot > result.slot:
result = head.justified.blck
proc preInit*(
T: type BlockPool, db: BeaconChainDB, state: BeaconState, blck: BeaconBlock) =

View File

@ -13,8 +13,9 @@ func ceil_div8(v: int): int = (v + 7) div 8
func init*(T: type BitField, bits: int): BitField =
BitField(bits: newSeq[byte](ceil_div8(bits)))
proc readValue*(r: var JsonReader, a: var BitField) {.inline.} =
a.bits = r.readValue(string).hexToSeqByte()
# TODO fix this for state tests..
#proc readValue*(r: var JsonReader, a: var BitField) {.inline.} =
# a.bits = r.readValue(string).hexToSeqByte()
# https://github.com/ethereum/eth2.0-specs/blob/v0.6.0/specs/core/0_beacon-chain.md#get_bitfield_bit
func get_bitfield_bit*(bitfield: BitField, i: int): bool =

View File

@ -1,7 +1,7 @@
import
options, tables,
chronicles, chronos, ranges/bitranges,
spec/[datatypes, crypto, digest], eth/rlp,
spec/[datatypes, crypto, digest, helpers], eth/rlp,
beacon_node_types, eth2_network, beacon_chain_db, block_pool, time, ssz
from beacon_node import onBeaconBlock
@ -81,17 +81,14 @@ p2pProtocol BeaconSync(version = 1,
node = peer.networkState.node
networkId = peer.networkState.networkId
blockPool = node.blockPool
latestState = blockPool.latestState()
headBlock = blockPool.head
var
latestFinalizedRoot: Eth2Digest # TODO
latestFinalizedEpoch = latestState.finalized_epoch
bestRoot: Eth2Digest # TODO
finalizedHead = blockPool.finalizedHead
headBlock = blockPool.head.blck
bestRoot = headBlock.root
bestSlot = headBlock.slot
latestFinalizedEpoch = finalizedHead.slot.slot_to_epoch()
let m = await handshake(peer, timeout = 10.seconds,
status(networkId, latestFinalizedRoot,
status(networkId, finalizedHead.blck.root,
latestFinalizedEpoch, bestRoot, bestSlot))
if m.networkId != networkId:
@ -169,7 +166,7 @@ p2pProtocol BeaconSync(version = 1,
var s = fromSlot
var roots = newSeqOfCap[(Eth2Digest, Slot)](maxRoots)
let blockPool = peer.networkState.node.blockPool
let maxSlot = blockPool.head.slot
let maxSlot = blockPool.head.blck.slot
while s <= maxSlot:
for r in blockPool.blockRootsForSlot(s):
roots.add((r, s))
@ -192,7 +189,7 @@ p2pProtocol BeaconSync(version = 1,
var headers = newSeqOfCap[BeaconBlockHeaderRLP](maxHeaders)
let db = peer.networkState.db
let blockPool = peer.networkState.node.blockPool
let maxSlot = blockPool.head.slot
let maxSlot = blockPool.head.blck.slot
while s <= maxSlot:
for r in blockPool.blockRootsForSlot(s):
headers.add(db.getBlock(r).get().toHeader)