keep cache of per-epoch items in block pool (#1068)

* plumbing between block pool and state transition functions around active validator indices and committees

* have shared epochrefs followed by blockref tree while allowing for skipped slots

* factor out the epoch info extraction; document how the EpochRef follows forks
This commit is contained in:
tersec 2020-05-29 06:10:20 +00:00 committed by GitHub
parent 4b731b266d
commit b5f45db5e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 6 deletions

View File

@ -116,6 +116,10 @@ type
updateFlags*: UpdateFlags updateFlags*: UpdateFlags
EpochRef* = ref object
shuffled_active_validator_indices*: seq[ValidatorIndex]
epoch*: Epoch
BlockRef* = ref object BlockRef* = ref object
## Node in object graph guaranteed to lead back to tail block, and to have ## Node in object graph guaranteed to lead back to tail block, and to have
## a corresponding entry in database. ## a corresponding entry in database.
@ -132,6 +136,11 @@ type
slot*: Slot # TODO could calculate this by walking to root, but.. slot*: Slot # TODO could calculate this by walking to root, but..
epochsInfo*: seq[EpochRef]
## Could be multiple, since blocks could skip slots, but usually, not many
## Even if competing forks happen later during this epoch, potential empty
## slots beforehand must all be from this fork.
BlockData* = object BlockData* = object
## Body and graph in one ## Body and graph in one

View File

@ -8,7 +8,7 @@
{.push raises: [Defect].} {.push raises: [Defect].}
import import
chronicles, options, tables, chronicles, options, sequtils, tables,
metrics, metrics,
../ssz, ../beacon_chain_db, ../state_transition, ../extras, ../ssz, ../beacon_chain_db, ../state_transition, ../extras,
../spec/[crypto, datatypes, digest, helpers, validator], ../spec/[crypto, datatypes, digest, helpers, validator],
@ -53,6 +53,12 @@ func parent*(bs: BlockSlot): BlockSlot =
slot: bs.slot - 1 slot: bs.slot - 1
) )
func populateEpochCache*(state: BeaconState, epoch: Epoch): EpochRef =
result = (EpochRef)(
epoch: state.slot.compute_epoch_at_slot,
shuffled_active_validator_indices:
get_shuffled_active_validator_indices(state, epoch))
func link*(parent, child: BlockRef) = func link*(parent, child: BlockRef) =
doAssert (not (parent.root == Eth2Digest() or child.root == Eth2Digest())), doAssert (not (parent.root == Eth2Digest() or child.root == Eth2Digest())),
"blocks missing root!" "blocks missing root!"
@ -133,6 +139,23 @@ func atSlot*(blck: BlockRef, slot: Slot): BlockSlot =
## block proposal) ## block proposal)
BlockSlot(blck: blck.getAncestorAt(slot), slot: slot) BlockSlot(blck: blck.getAncestorAt(slot), slot: slot)
func getEpochInfo*(blck: BlockRef, state: BeaconState): EpochRef =
# This is the only intended mechanism by which to get an EpochRef
let
state_epoch = state.slot.compute_epoch_at_slot
matching_epochinfo = blck.epochsInfo.filterIt(it.epoch == state_epoch)
if matching_epochinfo.len == 0:
let cache = populateEpochCache(state, state_epoch)
blck.epochsInfo.add(cache)
trace "candidate_chains.skipAndUpdateState(): back-filling parent.epochInfo",
state_slot = state.slot
cache
elif matching_epochinfo.len == 1:
matching_epochinfo[0]
else:
raiseAssert "multiple EpochRefs per epoch per BlockRef invalid"
func init(T: type BlockRef, root: Eth2Digest, slot: Slot): BlockRef = func init(T: type BlockRef, root: Eth2Digest, slot: Slot): BlockRef =
BlockRef( BlockRef(
root: root, root: root,
@ -441,8 +464,17 @@ proc skipAndUpdateState(
doAssert (addr(statePtr.data) == addr v) doAssert (addr(statePtr.data) == addr v)
statePtr[] = dag.headState statePtr[] = dag.headState
# TODO it's probably not the right way to convey this, but for now, avoids
# death-by-dozens-of-pointless-changes in developing this
let epochInfo = getEpochInfo(blck.refs, state.data.data)
var stateCache = get_empty_per_epoch_cache()
stateCache.shuffled_active_validator_indices[
state.data.data.slot.compute_epoch_at_slot] =
epochInfo.shuffled_active_validator_indices
let ok = state_transition( let ok = state_transition(
state.data, blck.data, flags + dag.updateFlags, restore) state.data, blck.data, stateCache, flags + dag.updateFlags, restore)
if ok and save: if ok and save:
dag.putState(state.data, blck.refs) dag.putState(state.data, blck.refs)

View File

@ -9,7 +9,7 @@ import
chronicles, tables, chronicles, tables,
metrics, stew/results, metrics, stew/results,
../ssz, ../state_transition, ../extras, ../ssz, ../state_transition, ../extras,
../spec/[crypto, datatypes, digest, helpers], ../spec/[crypto, datatypes, digest, helpers, validator],
block_pools_types, candidate_chains block_pools_types, candidate_chains
@ -46,6 +46,8 @@ proc addResolvedBlock(
doAssert state.slot == signedBlock.message.slot, "state must match block" doAssert state.slot == signedBlock.message.slot, "state must match block"
let blockRef = BlockRef.init(blockRoot, signedBlock.message) let blockRef = BlockRef.init(blockRoot, signedBlock.message)
parent.epochsInfo =
@[populateEpochCache(state, state.slot.compute_epoch_at_slot)]
link(parent, blockRef) link(parent, blockRef)
dag.blocks[blockRoot] = blockRef dag.blocks[blockRoot] = blockRef
@ -175,8 +177,19 @@ proc add*(
doAssert v.addr == addr poolPtr.tmpState.data doAssert v.addr == addr poolPtr.tmpState.data
poolPtr.tmpState = poolPtr.headState poolPtr.tmpState = poolPtr.headState
# TODO it's probably not the right way to convey this, but for now, avoids
# death-by-dozens-of-pointless-changes in developing this
# TODO rename these, since now, the two "state cache"s are juxtaposed
# directly
let epochInfo = getEpochInfo(parent, dag.tmpState.data.data)
var stateCache = get_empty_per_epoch_cache()
stateCache.shuffled_active_validator_indices[
dag.tmpState.data.data.slot.compute_epoch_at_slot] =
epochInfo.shuffled_active_validator_indices
# End of section to refactor/combine
if not state_transition( if not state_transition(
dag.tmpState.data, signedBlock, dag.updateFlags, restore): dag.tmpState.data, signedBlock, stateCache, dag.updateFlags, restore):
# TODO find a better way to log all this block data # TODO find a better way to log all this block data
notice "Invalid block", notice "Invalid block",
blck = shortLog(blck), blck = shortLog(blck),

View File

@ -399,11 +399,14 @@ type
data*: BeaconState data*: BeaconState
root*: Eth2Digest # hash_tree_root(data) root*: Eth2Digest # hash_tree_root(data)
# TODO remove some of these, or otherwise coordinate with EpochRef
StateCache* = object StateCache* = object
beacon_committee_cache*: beacon_committee_cache*:
Table[tuple[a: int, b: Eth2Digest], seq[ValidatorIndex]] Table[tuple[a: int, b: Eth2Digest], seq[ValidatorIndex]]
active_validator_indices_cache*: active_validator_indices_cache*:
Table[Epoch, seq[ValidatorIndex]] Table[Epoch, seq[ValidatorIndex]]
shuffled_active_validator_indices*:
Table[Epoch, seq[ValidatorIndex]]
committee_count_cache*: Table[Epoch, uint64] committee_count_cache*: Table[Epoch, uint64]
func shortValidatorKey*(state: BeaconState, validatorIdx: int): string = func shortValidatorKey*(state: BeaconState, validatorIdx: int): string =

View File

@ -79,6 +79,17 @@ func get_shuffled_seq*(seed: Eth2Digest,
result = shuffled_active_validator_indices result = shuffled_active_validator_indices
func get_shuffled_active_validator_indices*(state: BeaconState, epoch: Epoch):
seq[ValidatorIndex] =
# Non-spec function, to cache a data structure from which one can cheaply
# compute both get_active_validator_indexes() and get_beacon_committee().
let active_validator_indices = get_active_validator_indices(state, epoch)
mapIt(
get_shuffled_seq(
get_seed(state, epoch, DOMAIN_BEACON_ATTESTER),
active_validator_indices.len.uint64),
active_validator_indices[it])
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_previous_epoch # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#get_previous_epoch
func get_previous_epoch*(state: BeaconState): Epoch = func get_previous_epoch*(state: BeaconState): Epoch =
# Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``). # Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``).
@ -153,6 +164,8 @@ func get_empty_per_epoch_cache*(): StateCache =
initTable[tuple[a: int, b: Eth2Digest], seq[ValidatorIndex]]() initTable[tuple[a: int, b: Eth2Digest], seq[ValidatorIndex]]()
result.active_validator_indices_cache = result.active_validator_indices_cache =
initTable[Epoch, seq[ValidatorIndex]]() initTable[Epoch, seq[ValidatorIndex]]()
result.shuffled_active_validator_indices =
initTable[Epoch, seq[ValidatorIndex]]()
result.committee_count_cache = initTable[Epoch, uint64]() result.committee_count_cache = initTable[Epoch, uint64]()
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#compute_proposer_index # https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#compute_proposer_index

View File

@ -28,6 +28,7 @@
{.push raises: [Defect].} {.push raises: [Defect].}
import import
tables,
chronicles, chronicles,
stew/results, stew/results,
./extras, ./ssz, metrics, ./extras, ./ssz, metrics,
@ -177,6 +178,8 @@ proc noRollback*(state: var HashedBeaconState) =
proc state_transition*( proc state_transition*(
state: var HashedBeaconState, signedBlock: SignedBeaconBlock, state: var HashedBeaconState, signedBlock: SignedBeaconBlock,
# TODO this is ... okay, but not perfect; align with EpochRef
stateCache: var StateCache,
flags: UpdateFlags, rollback: RollbackHashedProc): bool {.nbench.} = flags: UpdateFlags, rollback: RollbackHashedProc): bool {.nbench.} =
## 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
@ -209,6 +212,7 @@ proc state_transition*(
# 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...)
doAssert not rollback.isNil, "use noRollback if it's ok to mess up state" doAssert not rollback.isNil, "use noRollback if it's ok to mess up state"
doAssert stateCache.shuffled_active_validator_indices.hasKey(state.data.slot.compute_epoch_at_slot)
if not process_slots(state, signedBlock.message.slot, flags): if not process_slots(state, signedBlock.message.slot, flags):
rollback(state) rollback(state)
@ -223,11 +227,11 @@ proc state_transition*(
if skipBLSValidation in flags or if skipBLSValidation in flags or
verify_block_signature(state.data, signedBlock): verify_block_signature(state.data, signedBlock):
var per_epoch_cache = get_empty_per_epoch_cache() # TODO after checking scaffolding, remove this
trace "in state_transition: processing block, signature passed", trace "in state_transition: processing block, signature passed",
signature = signedBlock.signature, signature = signedBlock.signature,
blockRoot = hash_tree_root(signedBlock.message) blockRoot = hash_tree_root(signedBlock.message)
if processBlock(state.data, signedBlock.message, flags, per_epoch_cache): if processBlock(state.data, signedBlock.message, flags, stateCache):
if skipStateRootValidation in flags or verifyStateRoot(state.data, signedBlock.message): if skipStateRootValidation in flags or verifyStateRoot(state.data, signedBlock.message):
# State root is what it should be - we're done! # State root is what it should be - we're done!
@ -245,6 +249,18 @@ proc state_transition*(
false false
proc state_transition*(
state: var HashedBeaconState, signedBlock: SignedBeaconBlock,
flags: UpdateFlags, rollback: RollbackHashedProc): bool {.nbench.} =
# TODO consider moving this to testutils or similar, since non-testing
# and fuzzing code should always be coming from blockpool which should
# always be providing cache or equivalent
var cache = get_empty_per_epoch_cache()
cache.shuffled_active_validator_indices[state.data.slot.compute_epoch_at_slot] =
get_shuffled_active_validator_indices(
state.data, state.data.slot.compute_epoch_at_slot)
state_transition(state, signedBlock, cache, flags, rollback)
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md # https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/validator.md
# TODO There's more to do here - the spec has helpers that deal set up some of # TODO There's more to do here - the spec has helpers that deal set up some of
# the fields in here! # the fields in here!