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:
parent
72749f4d04
commit
6bcefc0e42
|
@ -112,7 +112,7 @@ proc containsBlock*(
|
||||||
|
|
||||||
proc containsState*(
|
proc containsState*(
|
||||||
db: BeaconChainDB, key: Eth2Digest): bool =
|
db: BeaconChainDB, key: Eth2Digest): bool =
|
||||||
db.backend.contains(subkey(BeaconBlock, key))
|
db.backend.contains(subkey(BeaconState, key))
|
||||||
|
|
||||||
iterator getAncestors*(db: BeaconChainDB, root: Eth2Digest):
|
iterator getAncestors*(db: BeaconChainDB, root: Eth2Digest):
|
||||||
tuple[root: Eth2Digest, blck: BeaconBlock] =
|
tuple[root: Eth2Digest, blck: BeaconBlock] =
|
||||||
|
|
|
@ -330,13 +330,13 @@ proc proposeBlock(node: BeaconNode,
|
||||||
var newBlock = BeaconBlock(
|
var newBlock = BeaconBlock(
|
||||||
slot: slot,
|
slot: slot,
|
||||||
parent_root: node.state.blck.root,
|
parent_root: node.state.blck.root,
|
||||||
randao_reveal: validator.genRandaoReveal(state, state.slot),
|
randao_reveal: validator.genRandaoReveal(state, slot),
|
||||||
eth1_data: node.mainchainMonitor.getBeaconBlockRef(),
|
eth1_data: node.mainchainMonitor.getBeaconBlockRef(),
|
||||||
signature: ValidatorSig(), # we need the rest of the block first!
|
signature: ValidatorSig(), # we need the rest of the block first!
|
||||||
body: blockBody)
|
body: blockBody)
|
||||||
|
|
||||||
let ok =
|
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?
|
doAssert ok # TODO: err, could this fail somehow?
|
||||||
|
|
||||||
newBlock.state_root = Eth2Digest(data: hash_tree_root(state))
|
newBlock.state_root = Eth2Digest(data: hash_tree_root(state))
|
||||||
|
@ -428,43 +428,40 @@ proc scheduleEpochActions(node: BeaconNode, epoch: Epoch) =
|
||||||
epoch = humaneEpochNum(epoch),
|
epoch = humaneEpochNum(epoch),
|
||||||
stateEpoch = humaneEpochNum(node.state.data.slot.slot_to_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
|
# Sanity check - verify that the current head block is not too far behind
|
||||||
if node.state.data.slot.slot_to_epoch() + 1 < epoch:
|
if node.state.data.slot.slot_to_epoch() + 1 < epoch:
|
||||||
# Normally, we update the head state lazily, just before making an
|
# We're hopelessly behind!
|
||||||
# attestation. However, if we skip scheduling attestations, we'll never
|
#
|
||||||
# run the head update - thus we make an attempt now:
|
# There's a few ways this can happen:
|
||||||
node.updateHead()
|
#
|
||||||
|
# * 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:
|
let
|
||||||
# We're still behind!
|
expectedSlot = node.state.data.getSlotFromTime()
|
||||||
#
|
nextSlot = expectedSlot + 1
|
||||||
# There's a few ways this can happen:
|
at = node.slotStart(nextSlot)
|
||||||
#
|
|
||||||
# * 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
|
notice "Delaying epoch scheduling, head too old - scheduling new attempt",
|
||||||
expectedSlot = node.state.data.getSlotFromTime()
|
stateSlot = humaneSlotNum(node.state.data.slot),
|
||||||
nextSlot = expectedSlot + 1
|
expectedEpoch = humaneEpochNum(epoch),
|
||||||
at = node.slotStart(nextSlot)
|
expectedSlot = humaneSlotNum(expectedSlot),
|
||||||
|
fromNow = (at - fastEpochTime()) div 1000
|
||||||
|
|
||||||
notice "Delaying epoch scheduling, head too old - scheduling new attempt",
|
addTimer(at) do (p: pointer):
|
||||||
stateSlot = humaneSlotNum(node.state.data.slot),
|
node.scheduleEpochActions(nextSlot.slot_to_epoch())
|
||||||
expectedEpoch = humaneEpochNum(epoch),
|
return
|
||||||
expectedSlot = humaneSlotNum(expectedSlot),
|
|
||||||
fromNow = (at - fastEpochTime()) div 1000
|
|
||||||
|
|
||||||
addTimer(at) do (p: pointer):
|
|
||||||
node.scheduleEpochActions(nextSlot.slot_to_epoch())
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO: is this necessary with the new shuffling?
|
# TODO: is this necessary with the new shuffling?
|
||||||
# see get_beacon_proposer_index
|
# see get_beacon_proposer_index
|
||||||
|
@ -580,7 +577,14 @@ proc onBeaconBlock(node: BeaconNode, blck: BeaconBlock) =
|
||||||
voluntary_exits = blck.body.voluntary_exits.len,
|
voluntary_exits = blck.body.voluntary_exits.len,
|
||||||
transfers = blck.body.transfers.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
|
# TODO the fact that add returns a bool that causes the parent block to be
|
||||||
# pre-emptively fetched is quite ugly - fix.
|
# pre-emptively fetched is quite ugly - fix.
|
||||||
node.fetchBlocks(@[blck.parent_root])
|
node.fetchBlocks(@[blck.parent_root])
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import
|
import
|
||||||
bitops, chronicles, options, sequtils, sets, tables,
|
bitops, chronicles, options, sequtils, sets, tables,
|
||||||
ssz, beacon_chain_db, state_transition, extras,
|
ssz, beacon_chain_db, state_transition, extras,
|
||||||
spec/[crypto, datatypes, digest]
|
spec/[crypto, datatypes, digest, helpers]
|
||||||
|
|
||||||
type
|
type
|
||||||
BlockPool* = ref object
|
BlockPool* = ref object
|
||||||
|
@ -140,10 +140,18 @@ proc init*(T: type BlockPool, db: BeaconChainDB): BlockPool =
|
||||||
db: db
|
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
|
## return false indicates that the block parent was missing and should be
|
||||||
## fetched
|
## 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)
|
doAssert blockRoot == hash_tree_root_final(blck)
|
||||||
|
|
||||||
# Already seen this block??
|
# Already seen this block??
|
||||||
|
@ -169,23 +177,40 @@ proc add*(pool: var BlockPool, blockRoot: Eth2Digest, blck: BeaconBlock): bool =
|
||||||
|
|
||||||
return true
|
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)
|
let parent = pool.blocks.getOrDefault(blck.parent_root)
|
||||||
|
|
||||||
if parent != nil:
|
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(
|
let blockRef = BlockRef(
|
||||||
root: blockRoot
|
root: blockRoot
|
||||||
)
|
)
|
||||||
link(parent, blockRef)
|
link(parent, blockRef)
|
||||||
|
|
||||||
pool.blocks[blockRoot] = 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
|
# Resolved blocks should be stored in database
|
||||||
pool.db.putBlock(blockRoot, blck)
|
pool.db.putBlock(blockRoot, blck)
|
||||||
|
@ -209,7 +234,7 @@ proc add*(pool: var BlockPool, blockRoot: Eth2Digest, blck: BeaconBlock): bool =
|
||||||
# running out of stack etc
|
# running out of stack etc
|
||||||
let retries = pool.pending
|
let retries = pool.pending
|
||||||
for k, v in retries:
|
for k, v in retries:
|
||||||
discard pool.add(k, v)
|
discard pool.add(state, k, v)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
@ -271,6 +296,8 @@ proc checkUnresolved*(pool: var BlockPool): seq[Eth2Digest] =
|
||||||
inc v.tries
|
inc v.tries
|
||||||
|
|
||||||
for k in done:
|
for k in done:
|
||||||
|
# TODO Need to potentially remove from pool.pending - this is currently a
|
||||||
|
# memory leak here!
|
||||||
pool.unresolved.del(k)
|
pool.unresolved.del(k)
|
||||||
|
|
||||||
# simple (simplistic?) exponential backoff for retries..
|
# simple (simplistic?) exponential backoff for retries..
|
||||||
|
@ -279,24 +306,43 @@ proc checkUnresolved*(pool: var BlockPool): seq[Eth2Digest] =
|
||||||
result.add(k)
|
result.add(k)
|
||||||
|
|
||||||
proc skipAndUpdateState(
|
proc skipAndUpdateState(
|
||||||
state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags): bool =
|
state: var BeaconState, blck: BeaconBlock, flags: UpdateFlags,
|
||||||
skipSlots(state, blck.parent_root, blck.slot - 1)
|
afterUpdate: proc (state: BeaconState)): bool =
|
||||||
updateState(state, blck.parent_root, some(blck), flags)
|
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*(
|
proc updateState*(
|
||||||
pool: BlockPool, state: var StateData, blck: BlockRef) =
|
pool: BlockPool, state: var StateData, blck: BlockRef) =
|
||||||
if state.blck.root == blck.root:
|
# Rewind or advance state such that it matches the given block - this may
|
||||||
return # State already at the right spot
|
# include replaying from an earlier snapshot if blck is on a different branch
|
||||||
|
# or has advanced to a higher slot number than blck
|
||||||
# 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!"
|
|
||||||
|
|
||||||
var ancestors = @[pool.get(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
|
# Common case: blck points to a block that is one step ahead of state
|
||||||
if state.blck.root == blck.parent.root:
|
if state.blck.root == ancestors[0].data.parent_root and
|
||||||
let ok = skipAndUpdateState(state.data, ancestors[0].data, {skipValidation})
|
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.."
|
doAssert ok, "Blocks in database should never fail to apply.."
|
||||||
state.blck = blck
|
state.blck = blck
|
||||||
state.root = ancestors[0].data.state_root
|
state.root = ancestors[0].data.state_root
|
||||||
|
@ -329,6 +375,7 @@ proc updateState*(
|
||||||
|
|
||||||
notice "Replaying state transitions",
|
notice "Replaying state transitions",
|
||||||
stateSlot = humaneSlotNum(state.data.slot),
|
stateSlot = humaneSlotNum(state.data.slot),
|
||||||
|
stateRoot = shortLog(ancestor.data.state_root),
|
||||||
prevStateSlot = humaneSlotNum(ancestorState.get().slot),
|
prevStateSlot = humaneSlotNum(ancestorState.get().slot),
|
||||||
ancestors = ancestors.len
|
ancestors = ancestors.len
|
||||||
|
|
||||||
|
@ -345,18 +392,21 @@ proc updateState*(
|
||||||
for i in countdown(ancestors.len - 2, 0):
|
for i in countdown(ancestors.len - 2, 0):
|
||||||
let last = ancestors[i]
|
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(
|
let ok = updateState(
|
||||||
state.data, last.data.parent_root, some(last.data), {skipValidation})
|
state.data, last.data.parent_root, last.data, {skipValidation})
|
||||||
|
doAssert ok,
|
||||||
doAssert(ok)
|
"We only keep validated blocks in the database, should never fail"
|
||||||
|
|
||||||
state.blck = blck
|
state.blck = blck
|
||||||
state.root = ancestors[0].data.state_root
|
state.root = ancestors[0].data.state_root
|
||||||
|
|
||||||
|
pool.maybePutState(state.data)
|
||||||
|
|
||||||
proc loadTailState*(pool: BlockPool): StateData =
|
proc loadTailState*(pool: BlockPool): StateData =
|
||||||
## Load the state associated with the current tail in the pool
|
## Load the state associated with the current tail in the pool
|
||||||
StateData(
|
StateData(
|
||||||
|
|
|
@ -1006,14 +1006,16 @@ proc verifyStateRoot(state: BeaconState, blck: BeaconBlock): bool =
|
||||||
else:
|
else:
|
||||||
true
|
true
|
||||||
|
|
||||||
proc updateState*(state: var BeaconState, previous_block_root: Eth2Digest,
|
proc updateState*(
|
||||||
new_block: Option[BeaconBlock], flags: UpdateFlags): bool =
|
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,
|
## 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
|
## 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
|
## by the contents of a new block, but it may happen that the block goes
|
||||||
## missing - the state updates happen regardless.
|
## missing - the state updates happen regardless.
|
||||||
|
##
|
||||||
## Each call to this function will advance the state by one slot - new_block,
|
## 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
|
## 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
|
## 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
|
# 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
|
# the block proposer, though in reality we only need a partial update for
|
||||||
# that
|
# 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),
|
# 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
|
# for now, it serves as a reminder that we need to handle invalid blocks
|
||||||
# somewhere..
|
# 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
|
# the changes in case of failure (look out for `var BeaconState` and
|
||||||
# bool return values...)
|
# 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
|
var old_state = state
|
||||||
|
|
||||||
# Per-slot updates - these happen regardless if there is a block or not
|
# Per-slot updates - these happen regardless if there is a block or not
|
||||||
processSlot(state, previous_block_root)
|
processSlot(state, previous_block_root)
|
||||||
|
|
||||||
if new_block.isSome():
|
# Block updates - these happen when there's a new block being suggested
|
||||||
# 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
|
||||||
# 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
|
||||||
# according to the contents of this block - but first they will validate
|
# that the block is sane.
|
||||||
# that the block is sane.
|
# TODO what should happen if block processing fails?
|
||||||
# TODO what should happen if block processing fails?
|
# https://github.com/ethereum/eth2.0-specs/issues/293
|
||||||
# https://github.com/ethereum/eth2.0-specs/issues/293
|
if processBlock(state, new_block, flags):
|
||||||
if processBlock(state, new_block.get(), flags):
|
# Block ok so far, proceed with state update
|
||||||
# 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)
|
|
||||||
processEpoch(state)
|
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)
|
# This is a bit awkward - at the end of processing we verify that the
|
||||||
processEpoch(state)
|
# state we arrive at is what the block producer thought it would be -
|
||||||
true
|
# 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:
|
if state.slot < slot:
|
||||||
info "Advancing state past slot gap",
|
debug "Advancing state past slot gap",
|
||||||
targetSlot = humaneSlotNum(slot),
|
targetSlot = humaneSlotNum(slot),
|
||||||
stateSlot = humaneSlotNum(state.slot)
|
stateSlot = humaneSlotNum(state.slot)
|
||||||
|
|
||||||
while state.slot < slot:
|
while state.slot < slot:
|
||||||
let ok = updateState(state, parentRoot, none[BeaconBlock](), {})
|
advanceState(state, parentRoot)
|
||||||
doAssert ok, "Empty block state update should never fail!"
|
|
||||||
|
if not afterSlot.isNil:
|
||||||
|
afterSlot(state)
|
||||||
|
|
||||||
# TODO document this:
|
# TODO document this:
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ cli do(slots = 1945,
|
||||||
validators = SLOTS_PER_EPOCH, # One per shard is minimum
|
validators = SLOTS_PER_EPOCH, # One per shard is minimum
|
||||||
json_interval = SLOTS_PER_EPOCH,
|
json_interval = SLOTS_PER_EPOCH,
|
||||||
prefix = 0,
|
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):
|
validate = false):
|
||||||
let
|
let
|
||||||
flags = if validate: {} else: {skipValidation}
|
flags = if validate: {} else: {skipValidation}
|
||||||
|
|
|
@ -29,8 +29,7 @@ suite "Attestation pool processing":
|
||||||
pool = AttestationPool.init(blockPool)
|
pool = AttestationPool.init(blockPool)
|
||||||
state = blockPool.loadTailState()
|
state = blockPool.loadTailState()
|
||||||
# Slot 0 is a finalized slot - won't be making attestations for it..
|
# Slot 0 is a finalized slot - won't be making attestations for it..
|
||||||
discard updateState(
|
advanceState(state.data, state.blck.root)
|
||||||
state.data, state.blck.root, none(BeaconBlock), {skipValidation})
|
|
||||||
|
|
||||||
let
|
let
|
||||||
# Create an attestation for slot 1 signed by the only attester we have!
|
# Create an attestation for slot 1 signed by the only attester we have!
|
||||||
|
@ -47,14 +46,12 @@ suite "Attestation pool processing":
|
||||||
check:
|
check:
|
||||||
attestations.len == 1
|
attestations.len == 1
|
||||||
|
|
||||||
|
|
||||||
test "Attestations may arrive in any order":
|
test "Attestations may arrive in any order":
|
||||||
var
|
var
|
||||||
pool = AttestationPool.init(blockPool)
|
pool = AttestationPool.init(blockPool)
|
||||||
state = blockPool.loadTailState()
|
state = blockPool.loadTailState()
|
||||||
# Slot 0 is a finalized slot - won't be making attestations for it..
|
# Slot 0 is a finalized slot - won't be making attestations for it..
|
||||||
discard updateState(
|
advanceState(state.data, state.blck.root)
|
||||||
state.data, state.blck.root, none(BeaconBlock), {skipValidation})
|
|
||||||
|
|
||||||
let
|
let
|
||||||
# Create an attestation for slot 1 signed by the only attester we have!
|
# Create an attestation for slot 1 signed by the only attester we have!
|
||||||
|
@ -63,8 +60,7 @@ suite "Attestation pool processing":
|
||||||
attestation1 = makeAttestation(
|
attestation1 = makeAttestation(
|
||||||
state.data, state.blck.root, crosslink_committees1[0].committee[0])
|
state.data, state.blck.root, crosslink_committees1[0].committee[0])
|
||||||
|
|
||||||
discard updateState(
|
advanceState(state.data, state.blck.root)
|
||||||
state.data, state.blck.root, none(BeaconBlock), {skipValidation})
|
|
||||||
|
|
||||||
let
|
let
|
||||||
crosslink_committees2 =
|
crosslink_committees2 =
|
||||||
|
|
|
@ -10,18 +10,50 @@ import options, unittest, sequtils, strutils, eth/trie/[db],
|
||||||
../beacon_chain/spec/[datatypes, digest, crypto]
|
../beacon_chain/spec/[datatypes, digest, crypto]
|
||||||
|
|
||||||
suite "Beacon chain DB":
|
suite "Beacon chain DB":
|
||||||
var
|
|
||||||
db = init(BeaconChainDB, newMemoryDB())
|
|
||||||
|
|
||||||
test "empty database":
|
test "empty database":
|
||||||
|
var
|
||||||
|
db = init(BeaconChainDB, newMemoryDB())
|
||||||
|
|
||||||
check:
|
check:
|
||||||
db.getState(Eth2Digest()).isNone
|
db.getState(Eth2Digest()).isNone
|
||||||
db.getBlock(Eth2Digest()).isNone
|
db.getBlock(Eth2Digest()).isNone
|
||||||
|
|
||||||
test "find ancestors":
|
test "sanity check blocks":
|
||||||
var x: ValidatorSig
|
var
|
||||||
var y = init(ValidatorSig, x.getBytes())
|
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
|
check: x == y
|
||||||
|
|
||||||
let
|
let
|
||||||
|
|
|
@ -12,7 +12,7 @@ import
|
||||||
../beacon_chain/[block_pool, beacon_chain_db, extras, state_transition, ssz]
|
../beacon_chain/[block_pool, beacon_chain_db, extras, state_transition, ssz]
|
||||||
|
|
||||||
suite "Block pool processing":
|
suite "Block pool processing":
|
||||||
var
|
let
|
||||||
genState = get_genesis_beacon_state(
|
genState = get_genesis_beacon_state(
|
||||||
makeInitialDeposits(flags = {skipValidation}), 0, Eth1Data(),
|
makeInitialDeposits(flags = {skipValidation}), 0, Eth1Data(),
|
||||||
{skipValidation})
|
{skipValidation})
|
||||||
|
@ -38,7 +38,7 @@ suite "Block pool processing":
|
||||||
b1Root = hash_tree_root_final(b1)
|
b1Root = hash_tree_root_final(b1)
|
||||||
|
|
||||||
# TODO the return value is ugly here, need to fix and test..
|
# 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)
|
let b1Ref = pool.get(b1Root)
|
||||||
|
|
||||||
|
@ -53,19 +53,18 @@ suite "Block pool processing":
|
||||||
state = pool.loadTailState()
|
state = pool.loadTailState()
|
||||||
|
|
||||||
let
|
let
|
||||||
b1 = addBlock(
|
b1 = addBlock(state.data, state.blck.root, BeaconBlockBody(), {})
|
||||||
state.data, state.blck.root, BeaconBlockBody(), {skipValidation})
|
|
||||||
b1Root = hash_tree_root_final(b1)
|
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)
|
b2Root = hash_tree_root_final(b2)
|
||||||
|
|
||||||
discard pool.add(b2Root, b2)
|
discard pool.add(state, b2Root, b2)
|
||||||
|
|
||||||
check:
|
check:
|
||||||
pool.get(b2Root).isNone() # Unresolved, shouldn't show up
|
pool.get(b2Root).isNone() # Unresolved, shouldn't show up
|
||||||
b1Root in pool.checkUnresolved()
|
b1Root in pool.checkUnresolved()
|
||||||
|
|
||||||
discard pool.add(b1Root, b1)
|
discard pool.add(state, b1Root, b1)
|
||||||
|
|
||||||
let
|
let
|
||||||
b1r = pool.get(b1Root)
|
b1r = pool.get(b1Root)
|
||||||
|
|
|
@ -25,24 +25,19 @@ suite "Block processing":
|
||||||
test "Passes from genesis state, no block":
|
test "Passes from genesis state, no block":
|
||||||
var
|
var
|
||||||
state = genesisState
|
state = genesisState
|
||||||
proposer_index = getNextBeaconProposerIndex(state)
|
|
||||||
previous_block_root = hash_tree_root_final(genesisBlock)
|
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
|
state.slot == genesisState.slot + 1
|
||||||
|
|
||||||
test "Passes from genesis state, empty block":
|
test "Passes from genesis state, empty block":
|
||||||
var
|
var
|
||||||
state = genesisState
|
state = genesisState
|
||||||
proposer_index = getNextBeaconProposerIndex(state)
|
|
||||||
previous_block_root = hash_tree_root_final(genesisBlock)
|
previous_block_root = hash_tree_root_final(genesisBlock)
|
||||||
new_block = makeBlock(state, previous_block_root, BeaconBlockBody())
|
new_block = makeBlock(state, previous_block_root, BeaconBlockBody())
|
||||||
|
|
||||||
let block_ok = updateState(
|
let block_ok = updateState(state, previous_block_root, new_block, {})
|
||||||
state, previous_block_root, some(new_block), {})
|
|
||||||
|
|
||||||
check:
|
check:
|
||||||
block_ok
|
block_ok
|
||||||
|
@ -55,10 +50,7 @@ suite "Block processing":
|
||||||
previous_block_root = hash_tree_root_final(genesisBlock)
|
previous_block_root = hash_tree_root_final(genesisBlock)
|
||||||
|
|
||||||
for i in 1..SLOTS_PER_EPOCH.int:
|
for i in 1..SLOTS_PER_EPOCH.int:
|
||||||
let block_ok = updateState(
|
advanceState(state, previous_block_root)
|
||||||
state, previous_block_root, none(BeaconBlock), {})
|
|
||||||
check:
|
|
||||||
block_ok
|
|
||||||
|
|
||||||
check:
|
check:
|
||||||
state.slot == genesisState.slot + SLOTS_PER_EPOCH
|
state.slot == genesisState.slot + SLOTS_PER_EPOCH
|
||||||
|
@ -72,7 +64,7 @@ suite "Block processing":
|
||||||
var new_block = makeBlock(state, previous_block_root, BeaconBlockBody())
|
var new_block = makeBlock(state, previous_block_root, BeaconBlockBody())
|
||||||
|
|
||||||
let block_ok = updateState(
|
let block_ok = updateState(
|
||||||
state, previous_block_root, some(new_block), {})
|
state, previous_block_root, new_block, {})
|
||||||
|
|
||||||
check:
|
check:
|
||||||
block_ok
|
block_ok
|
||||||
|
@ -88,8 +80,7 @@ suite "Block processing":
|
||||||
previous_block_root = hash_tree_root_final(genesisBlock)
|
previous_block_root = hash_tree_root_final(genesisBlock)
|
||||||
|
|
||||||
# Slot 0 is a finalized slot - won't be making attestations for it..
|
# Slot 0 is a finalized slot - won't be making attestations for it..
|
||||||
discard updateState(
|
advanceState(state, previous_block_root)
|
||||||
state, previous_block_root, none(BeaconBlock), {})
|
|
||||||
|
|
||||||
let
|
let
|
||||||
# Create an attestation for slot 1 signed by the only attester we have!
|
# 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
|
# Some time needs to pass before attestations are included - this is
|
||||||
# to let the attestation propagate properly to interested participants
|
# to let the attestation propagate properly to interested participants
|
||||||
while state.slot < GENESIS_SLOT + MIN_ATTESTATION_INCLUSION_DELAY + 1:
|
while state.slot < GENESIS_SLOT + MIN_ATTESTATION_INCLUSION_DELAY + 1:
|
||||||
discard updateState(
|
advanceState(state, previous_block_root)
|
||||||
state, previous_block_root, none(BeaconBlock), {})
|
|
||||||
|
|
||||||
let
|
let
|
||||||
new_block = makeBlock(state, previous_block_root, BeaconBlockBody(
|
new_block = makeBlock(state, previous_block_root, BeaconBlockBody(
|
||||||
attestations: @[attestation]
|
attestations: @[attestation]
|
||||||
))
|
))
|
||||||
discard updateState(state, previous_block_root, some(new_block), {})
|
discard updateState(state, previous_block_root, new_block, {})
|
||||||
|
|
||||||
check:
|
check:
|
||||||
state.latest_attestations.len == 1
|
state.latest_attestations.len == 1
|
||||||
|
|
||||||
while state.slot < 191:
|
while state.slot < 191:
|
||||||
discard updateState(
|
advanceState(state, previous_block_root)
|
||||||
state, previous_block_root, none(BeaconBlock), {})
|
|
||||||
|
|
||||||
# Would need to process more epochs for the attestation to be removed from
|
# Would need to process more epochs for the attestation to be removed from
|
||||||
# the state! (per above bug)
|
# the state! (per above bug)
|
||||||
|
|
|
@ -107,7 +107,7 @@ proc addBlock*(
|
||||||
)
|
)
|
||||||
|
|
||||||
let block_ok = updateState(
|
let block_ok = updateState(
|
||||||
state, previous_block_root, some(new_block), {skipValidation})
|
state, previous_block_root, new_block, {skipValidation})
|
||||||
assert block_ok
|
assert block_ok
|
||||||
|
|
||||||
# Ok, we have the new state as it would look with the block applied - now we
|
# 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,
|
shard: sac.shard,
|
||||||
beacon_block_root: beacon_block_root,
|
beacon_block_root: beacon_block_root,
|
||||||
epoch_boundary_root: Eth2Digest(), # TODO
|
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
|
shard_block_root: Eth2Digest(), # TODO
|
||||||
justified_epoch: state.justified_epoch,
|
justified_epoch: state.justified_epoch,
|
||||||
justified_block_root: get_block_root(state, get_epoch_start_slot(state.justified_epoch)),
|
justified_block_root: get_block_root(state, get_epoch_start_slot(state.justified_epoch)),
|
||||||
|
|
Loading…
Reference in New Issue