verify blocks before storing in block pool / database (#158)

* fix state db lookup typo
* fix randao reveal slot when proposing blocks
* only store blocks that can be applied to a state
* store state at every epoch boundary (yes, needs pruning!)
* split out state advancement function when there's no block
* default state sim to 0.9 attestation ratio
This commit is contained in:
Jacek Sieka 2019-03-08 10:40:17 -06:00 committed by GitHub
parent 72749f4d04
commit 6bcefc0e42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 226 additions and 146 deletions

View File

@ -112,7 +112,7 @@ proc containsBlock*(
proc containsState*(
db: BeaconChainDB, key: Eth2Digest): bool =
db.backend.contains(subkey(BeaconBlock, key))
db.backend.contains(subkey(BeaconState, key))
iterator getAncestors*(db: BeaconChainDB, root: Eth2Digest):
tuple[root: Eth2Digest, blck: BeaconBlock] =

View File

@ -330,13 +330,13 @@ proc proposeBlock(node: BeaconNode,
var newBlock = BeaconBlock(
slot: slot,
parent_root: node.state.blck.root,
randao_reveal: validator.genRandaoReveal(state, state.slot),
randao_reveal: validator.genRandaoReveal(state, slot),
eth1_data: node.mainchainMonitor.getBeaconBlockRef(),
signature: ValidatorSig(), # we need the rest of the block first!
body: blockBody)
let ok =
updateState(state, node.state.blck.root, some(newBlock), {skipValidation})
updateState(state, node.state.blck.root, newBlock, {skipValidation})
doAssert ok # TODO: err, could this fail somehow?
newBlock.state_root = Eth2Digest(data: hash_tree_root(state))
@ -428,43 +428,40 @@ proc scheduleEpochActions(node: BeaconNode, epoch: Epoch) =
epoch = humaneEpochNum(epoch),
stateEpoch = humaneEpochNum(node.state.data.slot.slot_to_epoch())
# In case some late blocks dropped in
node.updateHead()
# Sanity check - verify that the current head block is not too far behind
if node.state.data.slot.slot_to_epoch() + 1 < epoch:
# Normally, we update the head state lazily, just before making an
# attestation. However, if we skip scheduling attestations, we'll never
# run the head update - thus we make an attempt now:
node.updateHead()
# We're hopelessly behind!
#
# There's a few ways this can happen:
#
# * we receive no attestations or blocks for an extended period of time
# * all the attestations we receive are bogus - maybe we're connected to
# the wrong network?
# * we just started and still haven't synced
#
# TODO make an effort to find other nodes and sync? A worst case scenario
# here is that the network stalls because nobody is sending out
# attestations because nobody is scheduling them, in a vicious
# circle
# TODO diagnose the various scenarios and do something smart...
if node.state.data.slot.slot_to_epoch() + 1 < epoch:
# We're still behind!
#
# There's a few ways this can happen:
#
# * we receive no attestations or blocks for an extended period of time
# * all the attestations we receive are bogus - maybe we're connected to
# the wrong network?
# * we just started and still haven't synced
#
# TODO make an effort to find other nodes and sync? A worst case scenario
# here is that the network stalls because nobody is sending out
# attestations because nobody is scheduling them, in a vicious
# circle
# TODO diagnose the various scenarios and do something smart...
let
expectedSlot = node.state.data.getSlotFromTime()
nextSlot = expectedSlot + 1
at = node.slotStart(nextSlot)
let
expectedSlot = node.state.data.getSlotFromTime()
nextSlot = expectedSlot + 1
at = node.slotStart(nextSlot)
notice "Delaying epoch scheduling, head too old - scheduling new attempt",
stateSlot = humaneSlotNum(node.state.data.slot),
expectedEpoch = humaneEpochNum(epoch),
expectedSlot = humaneSlotNum(expectedSlot),
fromNow = (at - fastEpochTime()) div 1000
notice "Delaying epoch scheduling, head too old - scheduling new attempt",
stateSlot = humaneSlotNum(node.state.data.slot),
expectedEpoch = humaneEpochNum(epoch),
expectedSlot = humaneSlotNum(expectedSlot),
fromNow = (at - fastEpochTime()) div 1000
addTimer(at) do (p: pointer):
node.scheduleEpochActions(nextSlot.slot_to_epoch())
return
addTimer(at) do (p: pointer):
node.scheduleEpochActions(nextSlot.slot_to_epoch())
return
# TODO: is this necessary with the new shuffling?
# see get_beacon_proposer_index
@ -580,7 +577,14 @@ proc onBeaconBlock(node: BeaconNode, blck: BeaconBlock) =
voluntary_exits = blck.body.voluntary_exits.len,
transfers = blck.body.transfers.len
if not node.blockPool.add(blockRoot, blck):
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):
# 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])

View File

@ -1,7 +1,7 @@
import
bitops, chronicles, options, sequtils, sets, tables,
ssz, beacon_chain_db, state_transition, extras,
spec/[crypto, datatypes, digest]
spec/[crypto, datatypes, digest, helpers]
type
BlockPool* = ref object
@ -140,10 +140,18 @@ proc init*(T: type BlockPool, db: BeaconChainDB): BlockPool =
db: db
)
proc add*(pool: var BlockPool, blockRoot: Eth2Digest, blck: BeaconBlock): bool =
proc updateState*(
pool: BlockPool, state: var StateData, blck: BlockRef) {.gcsafe.}
proc add*(
pool: var BlockPool, state: var StateData, blockRoot: Eth2Digest,
blck: BeaconBlock): bool {.gcsafe.} =
## return false indicates that the block parent was missing and should be
## fetched
## TODO reevaluate this API - it's pretty ugly with the bool return
## the state parameter may be updated to include the given block, if
## everything checks out
# TODO reevaluate passing the state in like this
# TODO reevaluate this API - it's pretty ugly with the bool return
doAssert blockRoot == hash_tree_root_final(blck)
# Already seen this block??
@ -169,23 +177,40 @@ proc add*(pool: var BlockPool, blockRoot: Eth2Digest, blck: BeaconBlock): bool =
return true
# TODO we should now validate the block to ensure that it's sane - but the
# only way to do that is to apply it to the state... for now, we assume
# all blocks are good!
let parent = pool.blocks.getOrDefault(blck.parent_root)
if parent != nil:
# The block is resolved, nothing more to do!
# The block might have been in either of these - we don't want any more
# work done on its behalf
pool.unresolved.del(blockRoot)
pool.pending.del(blockRoot)
# The block is resolved, now it's time to validate it to ensure that the
# blocks we add to the database are clean for the given state
updateState(pool, state, parent)
skipSlots(state.data, parent.root, blck.slot - 1)
if not updateState(state.data, parent.root, blck, {}):
# TODO find a better way to log all this block data
notice "Invalid block",
blockRoot = shortLog(blockRoot),
slot = humaneSlotNum(blck.slot),
stateRoot = shortLog(blck.state_root),
parentRoot = shortLog(blck.parent_root),
signature = shortLog(blck.signature),
proposer_slashings = blck.body.proposer_slashings.len,
attester_slashings = blck.body.attester_slashings.len,
attestations = blck.body.attestations.len,
deposits = blck.body.deposits.len,
voluntary_exits = blck.body.voluntary_exits.len,
transfers = blck.body.transfers.len
let blockRef = BlockRef(
root: blockRoot
)
link(parent, blockRef)
pool.blocks[blockRoot] = blockRef
# The block might have been in either of these - we don't want any more
# work done on its behalf
pool.unresolved.del(blockRoot)
pool.pending.del(blockRoot)
# Resolved blocks should be stored in database
pool.db.putBlock(blockRoot, blck)
@ -209,7 +234,7 @@ proc add*(pool: var BlockPool, blockRoot: Eth2Digest, blck: BeaconBlock): bool =
# running out of stack etc
let retries = pool.pending
for k, v in retries:
discard pool.add(k, v)
discard pool.add(state, k, v)
return true
@ -271,6 +296,8 @@ proc checkUnresolved*(pool: var BlockPool): seq[Eth2Digest] =
inc v.tries
for k in done:
# TODO Need to potentially remove from pool.pending - this is currently a
# memory leak here!
pool.unresolved.del(k)
# simple (simplistic?) exponential backoff for retries..
@ -279,24 +306,43 @@ proc checkUnresolved*(pool: var BlockPool): seq[Eth2Digest] =
result.add(k)
proc skipAndUpdateState(
state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags): bool =
skipSlots(state, blck.parent_root, blck.slot - 1)
updateState(state, blck.parent_root, some(blck), flags)
state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags,
afterUpdate: proc (state: BeaconState)): bool =
skipSlots(state, blck.parent_root, blck.slot - 1, afterUpdate)
let ok = updateState(state, blck.parent_root, blck, flags)
afterUpdate(state)
ok
proc maybePutState(pool: BlockPool, state: BeaconState) =
# TODO we save state at every epoch start but never remove them - we also
# potentially save multiple states per slot if reorgs happen, meaning
# we could easily see a state explosion
if state.slot mod SLOTS_PER_EPOCH == 0:
info "Storing state",
stateSlot = humaneSlotNum(state.slot),
stateRoot = hash_tree_root_final(state) # TODO cache?
pool.db.putState(state)
proc updateState*(
pool: BlockPool, state: var StateData, blck: BlockRef) =
if state.blck.root == blck.root:
return # State already at the right spot
# TODO this blockref should never be created, since we trace every blockref
# back to the tail block
doAssert (not blck.parent.isNil), "trying to apply genesis block!"
# Rewind or advance state such that it matches the given block - this may
# include replaying from an earlier snapshot if blck is on a different branch
# or has advanced to a higher slot number than blck
var ancestors = @[pool.get(blck)]
# We need to check the slot because the state might have moved forwards
# without blocks
if state.blck.root == blck.root and state.data.slot == ancestors[0].data.slot:
return # State already at the right spot
# Common case: blck points to a block that is one step ahead of state
if state.blck.root == blck.parent.root:
let ok = skipAndUpdateState(state.data, ancestors[0].data, {skipValidation})
if state.blck.root == ancestors[0].data.parent_root and
state.data.slot + 1 == ancestors[0].data.slot:
let ok = skipAndUpdateState(
state.data, ancestors[0].data, {skipValidation}) do (state: BeaconState):
pool.maybePutState(state)
doAssert ok, "Blocks in database should never fail to apply.."
state.blck = blck
state.root = ancestors[0].data.state_root
@ -329,6 +375,7 @@ proc updateState*(
notice "Replaying state transitions",
stateSlot = humaneSlotNum(state.data.slot),
stateRoot = shortLog(ancestor.data.state_root),
prevStateSlot = humaneSlotNum(ancestorState.get().slot),
ancestors = ancestors.len
@ -345,18 +392,21 @@ proc updateState*(
for i in countdown(ancestors.len - 2, 0):
let last = ancestors[i]
skipSlots(state.data, last.data.parent_root, last.data.slot - 1)
skipSlots(
state.data, last.data.parent_root,
last.data.slot - 1) do(state: BeaconState):
pool.maybePutState(state)
# TODO technically, we should be adding states to the database here because
# we're going down a different fork..
let ok = updateState(
state.data, last.data.parent_root, some(last.data), {skipValidation})
doAssert(ok)
state.data, last.data.parent_root, last.data, {skipValidation})
doAssert ok,
"We only keep validated blocks in the database, should never fail"
state.blck = blck
state.root = ancestors[0].data.state_root
pool.maybePutState(state.data)
proc loadTailState*(pool: BlockPool): StateData =
## Load the state associated with the current tail in the pool
StateData(

View File

@ -1006,14 +1006,16 @@ proc verifyStateRoot(state: BeaconState, blck: BeaconBlock): bool =
else:
true
proc updateState*(state: var BeaconState, previous_block_root: Eth2Digest,
new_block: Option[BeaconBlock], flags: UpdateFlags): bool =
proc updateState*(
state: var BeaconState, previous_block_root: Eth2Digest,
new_block: BeaconBlock, flags: UpdateFlags): bool =
## Time in the beacon chain moves by slots. Every time (haha.) that happens,
## we will update the beacon state. Normally, the state updates will be driven
## by the contents of a new block, but it may happen that the block goes
## missing - the state updates happen regardless.
##
## Each call to this function will advance the state by one slot - new_block,
## if present, must match that slot.
## must match that slot. If the update fails, the state will remain unchanged.
##
## The flags are used to specify that certain validations should be skipped
## for the new block. This is done during block proposal, to create a state
@ -1025,59 +1027,67 @@ proc updateState*(state: var BeaconState, previous_block_root: Eth2Digest,
# One reason to keep it this way is that you need to look ahead if you're
# the block proposer, though in reality we only need a partial update for
# that
# TODO There's a discussion about what this function should do, and when:
# https://github.com/ethereum/eth2.0-specs/issues/284
# TODO check to which extent this copy can be avoided (considering forks etc),
# for now, it serves as a reminder that we need to handle invalid blocks
# somewhere..
# TODO many functions will mutate `state` partially without rolling back
# many functions will mutate `state` partially without rolling back
# the changes in case of failure (look out for `var BeaconState` and
# bool return values...)
# TODO There's a discussion about what this function should do, and when:
# https://github.com/ethereum/eth2.0-specs/issues/284
var old_state = state
# Per-slot updates - these happen regardless if there is a block or not
processSlot(state, previous_block_root)
if new_block.isSome():
# Block updates - these happen when there's a new block being suggested
# by the block proposer. Every actor in the network will update its state
# according to the contents of this block - but first they will validate
# that the block is sane.
# TODO what should happen if block processing fails?
# https://github.com/ethereum/eth2.0-specs/issues/293
if processBlock(state, new_block.get(), flags):
# Block ok so far, proceed with state update
processEpoch(state)
# This is a bit awkward - at the end of processing we verify that the
# state we arrive at is what the block producer thought it would be -
# meaning that potentially, it could fail verification
if skipValidation in flags or verifyStateRoot(state, new_block.get()):
# State root is what it should be - we're done!
return true
# Block processing failed, have to start over
state = old_state
processSlot(state, previous_block_root)
# Block updates - these happen when there's a new block being suggested
# by the block proposer. Every actor in the network will update its state
# according to the contents of this block - but first they will validate
# that the block is sane.
# TODO what should happen if block processing fails?
# https://github.com/ethereum/eth2.0-specs/issues/293
if processBlock(state, new_block, flags):
# Block ok so far, proceed with state update
processEpoch(state)
false
else:
# Skip all per-block processing. Move directly to epoch processing
# prison. Do not do any block updates when passing go.
# Heavy updates that happen for every epoch - these never fail (or so we hope)
processEpoch(state)
true
# This is a bit awkward - at the end of processing we verify that the
# state we arrive at is what the block producer thought it would be -
# meaning that potentially, it could fail verification
if skipValidation in flags or verifyStateRoot(state, new_block):
# State root is what it should be - we're done!
return true
proc skipSlots*(state: var BeaconState, parentRoot: Eth2Digest, slot: Slot) =
# Block processing failed, roll back changes
state = old_state
false
proc advanceState*(
state: var BeaconState, previous_block_root: Eth2Digest) =
## Sometimes we need to update the state even though we don't have a block at
## hand - this happens for example when a block proposer fails to produce a
## a block.
# TODO In the current spec, this can fail only when the state is inconsistent
# or buggy - how do we handle that? crash?
# Per-slot updates - these happen regardless if there is a block or not
processSlot(state, previous_block_root)
# Heavy updates that happen for every epoch - these never fail (or so we hope)
processEpoch(state)
proc skipSlots*(state: var BeaconState, parentRoot: Eth2Digest, slot: Slot,
afterSlot: proc (state: BeaconState) = nil) =
if state.slot < slot:
info "Advancing state past slot gap",
debug "Advancing state past slot gap",
targetSlot = humaneSlotNum(slot),
stateSlot = humaneSlotNum(state.slot)
while state.slot < slot:
let ok = updateState(state, parentRoot, none[BeaconBlock](), {})
doAssert ok, "Empty block state update should never fail!"
advanceState(state, parentRoot)
if not afterSlot.isNil:
afterSlot(state)
# TODO document this:

View File

@ -46,7 +46,7 @@ cli do(slots = 1945,
validators = SLOTS_PER_EPOCH, # One per shard is minimum
json_interval = SLOTS_PER_EPOCH,
prefix = 0,
attesterRatio {.desc: "ratio of validators that attest in each round"} = 0.0,
attesterRatio {.desc: "ratio of validators that attest in each round"} = 0.9,
validate = false):
let
flags = if validate: {} else: {skipValidation}

View File

@ -29,8 +29,7 @@ suite "Attestation pool processing":
pool = AttestationPool.init(blockPool)
state = blockPool.loadTailState()
# Slot 0 is a finalized slot - won't be making attestations for it..
discard updateState(
state.data, state.blck.root, none(BeaconBlock), {skipValidation})
advanceState(state.data, state.blck.root)
let
# Create an attestation for slot 1 signed by the only attester we have!
@ -47,14 +46,12 @@ suite "Attestation pool processing":
check:
attestations.len == 1
test "Attestations may arrive in any order":
var
pool = AttestationPool.init(blockPool)
state = blockPool.loadTailState()
# Slot 0 is a finalized slot - won't be making attestations for it..
discard updateState(
state.data, state.blck.root, none(BeaconBlock), {skipValidation})
advanceState(state.data, state.blck.root)
let
# Create an attestation for slot 1 signed by the only attester we have!
@ -63,8 +60,7 @@ suite "Attestation pool processing":
attestation1 = makeAttestation(
state.data, state.blck.root, crosslink_committees1[0].committee[0])
discard updateState(
state.data, state.blck.root, none(BeaconBlock), {skipValidation})
advanceState(state.data, state.blck.root)
let
crosslink_committees2 =

View File

@ -10,18 +10,50 @@ import options, unittest, sequtils, strutils, eth/trie/[db],
../beacon_chain/spec/[datatypes, digest, crypto]
suite "Beacon chain DB":
var
db = init(BeaconChainDB, newMemoryDB())
test "empty database":
var
db = init(BeaconChainDB, newMemoryDB())
check:
db.getState(Eth2Digest()).isNone
db.getBlock(Eth2Digest()).isNone
test "find ancestors":
var x: ValidatorSig
var y = init(ValidatorSig, x.getBytes())
test "sanity check blocks":
var
db = init(BeaconChainDB, newMemoryDB())
let
blck = BeaconBlock()
root = hash_tree_root_final(blck)
db.putBlock(blck)
check:
db.containsBlock(root)
db.getBlock(root).get() == blck
test "sanity check states":
var
db = init(BeaconChainDB, newMemoryDB())
let
state = BeaconState()
root = hash_tree_root_final(state)
db.putState(state)
check:
db.containsState(root)
db.getState(root).get() == state
test "find ancestors":
var
db = init(BeaconChainDB, newMemoryDB())
x: ValidatorSig
y = init(ValidatorSig, x.getBytes())
# Silly serialization check that fails without the right import
check: x == y
let

View File

@ -12,7 +12,7 @@ import
../beacon_chain/[block_pool, beacon_chain_db, extras, state_transition, ssz]
suite "Block pool processing":
var
let
genState = get_genesis_beacon_state(
makeInitialDeposits(flags = {skipValidation}), 0, Eth1Data(),
{skipValidation})
@ -38,7 +38,7 @@ suite "Block pool processing":
b1Root = hash_tree_root_final(b1)
# TODO the return value is ugly here, need to fix and test..
discard pool.add(b1Root, b1)
discard pool.add(state, b1Root, b1)
let b1Ref = pool.get(b1Root)
@ -53,19 +53,18 @@ suite "Block pool processing":
state = pool.loadTailState()
let
b1 = addBlock(
state.data, state.blck.root, BeaconBlockBody(), {skipValidation})
b1 = addBlock(state.data, state.blck.root, BeaconBlockBody(), {})
b1Root = hash_tree_root_final(b1)
b2 = addBlock(state.data, b1Root, BeaconBlockBody(), {skipValidation})
b2 = addBlock(state.data, b1Root, BeaconBlockBody(), {})
b2Root = hash_tree_root_final(b2)
discard pool.add(b2Root, b2)
discard pool.add(state, b2Root, b2)
check:
pool.get(b2Root).isNone() # Unresolved, shouldn't show up
b1Root in pool.checkUnresolved()
discard pool.add(b1Root, b1)
discard pool.add(state, b1Root, b1)
let
b1r = pool.get(b1Root)

View File

@ -25,24 +25,19 @@ suite "Block processing":
test "Passes from genesis state, no block":
var
state = genesisState
proposer_index = getNextBeaconProposerIndex(state)
previous_block_root = hash_tree_root_final(genesisBlock)
let block_ok =
updateState(state, previous_block_root, none(BeaconBlock), {})
check:
block_ok
advanceState(state, previous_block_root)
check:
state.slot == genesisState.slot + 1
test "Passes from genesis state, empty block":
var
state = genesisState
proposer_index = getNextBeaconProposerIndex(state)
previous_block_root = hash_tree_root_final(genesisBlock)
new_block = makeBlock(state, previous_block_root, BeaconBlockBody())
let block_ok = updateState(
state, previous_block_root, some(new_block), {})
let block_ok = updateState(state, previous_block_root, new_block, {})
check:
block_ok
@ -55,10 +50,7 @@ suite "Block processing":
previous_block_root = hash_tree_root_final(genesisBlock)
for i in 1..SLOTS_PER_EPOCH.int:
let block_ok = updateState(
state, previous_block_root, none(BeaconBlock), {})
check:
block_ok
advanceState(state, previous_block_root)
check:
state.slot == genesisState.slot + SLOTS_PER_EPOCH
@ -72,7 +64,7 @@ suite "Block processing":
var new_block = makeBlock(state, previous_block_root, BeaconBlockBody())
let block_ok = updateState(
state, previous_block_root, some(new_block), {})
state, previous_block_root, new_block, {})
check:
block_ok
@ -88,8 +80,7 @@ suite "Block processing":
previous_block_root = hash_tree_root_final(genesisBlock)
# Slot 0 is a finalized slot - won't be making attestations for it..
discard updateState(
state, previous_block_root, none(BeaconBlock), {})
advanceState(state, previous_block_root)
let
# Create an attestation for slot 1 signed by the only attester we have!
@ -101,21 +92,19 @@ suite "Block processing":
# Some time needs to pass before attestations are included - this is
# to let the attestation propagate properly to interested participants
while state.slot < GENESIS_SLOT + MIN_ATTESTATION_INCLUSION_DELAY + 1:
discard updateState(
state, previous_block_root, none(BeaconBlock), {})
advanceState(state, previous_block_root)
let
new_block = makeBlock(state, previous_block_root, BeaconBlockBody(
attestations: @[attestation]
))
discard updateState(state, previous_block_root, some(new_block), {})
discard updateState(state, previous_block_root, new_block, {})
check:
state.latest_attestations.len == 1
while state.slot < 191:
discard updateState(
state, previous_block_root, none(BeaconBlock), {})
advanceState(state, previous_block_root)
# Would need to process more epochs for the attestation to be removed from
# the state! (per above bug)

View File

@ -107,7 +107,7 @@ proc addBlock*(
)
let block_ok = updateState(
state, previous_block_root, some(new_block), {skipValidation})
state, previous_block_root, new_block, {skipValidation})
assert block_ok
# Ok, we have the new state as it would look with the block applied - now we
@ -174,7 +174,7 @@ proc makeAttestation*(
shard: sac.shard,
beacon_block_root: beacon_block_root,
epoch_boundary_root: Eth2Digest(), # TODO
latest_crosslink: Crosslink(epoch: state.latest_crosslinks[sac.shard].epoch), # TODO
latest_crosslink: state.latest_crosslinks[sac.shard],
shard_block_root: Eth2Digest(), # TODO
justified_epoch: state.justified_epoch,
justified_block_root: get_block_root(state, get_epoch_start_slot(state.justified_epoch)),