Pre-compute slot transition for clearance state

This way we perform the expensive epoch processing before the block
arrives.

Of course, this may lead to speculative misses which in turn lead to
replays - it's likely that in the case of a miss, we'll see a replay
regardless.
This commit is contained in:
Jacek Sieka 2021-05-29 20:56:30 +02:00 committed by zah
parent aa6177814e
commit df7bc87af5
3 changed files with 96 additions and 60 deletions

View File

@ -40,6 +40,9 @@ template asSigVerified(x: SignedBeaconBlock): SigVerifiedSignedBeaconBlock =
##
## This SHOULD be used in function calls to avoid expensive temporary.
## see https://github.com/status-im/nimbus-eth2/pull/2250#discussion_r562010679
static: # TODO See isomorphicCast
doAssert sizeof(SignedBeaconBlock) == sizeof(SigVerifiedSignedBeaconBlock)
cast[ptr SigVerifiedSignedBeaconBlock](signedBlock.unsafeAddr)[]
template asTrusted(x: SignedBeaconBlock or SigVerifiedBeaconBlock): TrustedSignedBeaconBlock =
@ -52,6 +55,9 @@ template asTrusted(x: SignedBeaconBlock or SigVerifiedBeaconBlock): TrustedSigne
##
## This SHOULD be used in function calls to avoid expensive temporary.
## see https://github.com/status-im/nimbus-eth2/pull/2250#discussion_r562010679
static: # TODO See isomorphicCast
doAssert sizeof(x) == sizeof(TrustedSignedBeaconBlock)
cast[ptr TrustedSignedBeaconBlock](signedBlock.unsafeAddr)[]
func getOrResolve*(dag: ChainDAGRef, quarantine: var QuarantineRef, root: Eth2Digest): BlockRef =
@ -90,49 +96,42 @@ proc addResolvedBlock(
let
blockRoot = trustedBlock.root
blockRef = BlockRef.init(blockRoot, trustedBlock.message)
blockEpoch = blockRef.slot.compute_epoch_at_slot()
startTick = Moment.now()
link(parent, blockRef)
var epochRef = dag.findEpochRef(parent, blockEpoch)
if epochRef == nil:
let prevEpochRef =
if blockEpoch < 1: nil else: dag.findEpochRef(parent, blockEpoch - 1)
epochRef = EpochRef.init(state, cache, prevEpochRef)
dag.addEpochRef(blockRef, epochRef)
let epochRefTick = Moment.now()
dag.blocks.incl(KeyedBlockRef.init(blockRef))
trace "Populating block dag", key = blockRoot, val = blockRef
# Resolved blocks should be stored in database
dag.putBlock(trustedBlock)
let putBlockTick = Moment.now()
var foundHead: BlockRef
var foundHead: bool
for head in dag.heads.mitems():
if head.isAncestorOf(blockRef):
head = blockRef
foundHead = head
foundHead = true
break
if foundHead.isNil:
foundHead = blockRef
dag.heads.add(foundHead)
if not foundHead:
dag.heads.add(blockRef)
# Up to here, state.data was referring to the new state after the block had
# been applied but the `blck` field was still set to the parent
state.blck = blockRef
# Getting epochRef with the state will potentially create a new EpochRef
let
epochRef = dag.getEpochRef(state, cache)
epochRefTick = Moment.now()
debug "Block resolved",
blck = shortLog(trustedBlock.message),
blockRoot = shortLog(blockRoot),
heads = dag.heads.len(),
stateDataDur, sigVerifyDur, stateVerifyDur,
epochRefDur = epochRefTick - startTick,
putBlockDur = putBlockTick - epochRefTick
state.blck = blockRef
putBlockDur = putBlockTick - startTick,
epochRefDur = epochRefTick - putBlockTick
# Notify others of the new block before processing the quarantine, such that
# notifications for parents happens before those of the children
@ -184,6 +183,21 @@ proc addRawBlockCheckStateTransition(
return (ValidationResult.Reject, Invalid)
return (ValidationResult.Accept, default(BlockError))
proc advanceClearanceState*(dag: var ChainDagRef) =
# When the chain is synced, the most likely block to be produced is the block
# right after head - we can exploit this assumption and advance the state
# to that slot before the block arrives, thus allowing us to do the expensive
# epoch transition ahead of time.
# Notably, we use the clearance state here because that's where the block will
# first be seen - later, this state will be copied to the head state!
if dag.clearanceState.blck.slot == getStateField(dag.clearanceState, slot):
let next = dag.clearanceState.blck.atSlot(
getStateField(dag.clearanceState, slot) + 1)
debug "Preparing clearance state for next block", next
var cache = StateCache()
updateStateData(dag, dag.clearanceState,next, true, cache)
proc addRawBlockKnownParent(
dag: var ChainDAGRef, quarantine: var QuarantineRef,
signedBlock: SignedBeaconBlock,
@ -244,11 +258,11 @@ proc addRawBlockKnownParent(
return err((ValidationResult.Reject, Invalid))
let sigVerifyTick = Moment.now()
static: doAssert sizeof(SignedBeaconBlock) == sizeof(SigVerifiedSignedBeaconBlock)
let (valRes, blockErr) = addRawBlockCheckStateTransition(
dag, quarantine, signedBlock.asSigVerified(), cache)
if valRes != ValidationResult.Accept:
return err((valRes, blockErr))
let stateVerifyTick = Moment.now()
# Careful, clearanceState.data has been updated but not blck - we need to
# create the BlockRef first!

View File

@ -458,35 +458,55 @@ proc init*(T: type ChainDAGRef,
res
proc addEpochRef*(dag: ChainDAGRef, blck: BlockRef, epochRef: EpochRef) =
# Because we put a cap on the number of epochRefs we store, we want to
# prune the least useful state - for now, we'll assume that to be the oldest
# epochRef we know about.
var
oldest = 0
ancestor = blck.epochAncestor(epochRef.epoch)
for x in 0..<dag.epochRefs.len:
let candidate = dag.epochRefs[x]
if candidate[1] == nil:
oldest = x
break
if candidate[1].epoch < dag.epochRefs[oldest][1].epoch:
oldest = x
proc getEpochRef*(
dag: ChainDAGRef, state: StateData, cache: var StateCache): EpochRef =
let
blck = state.blck
epoch = getStateField(state, slot).epoch
dag.epochRefs[oldest] = (ancestor.blck, epochRef)
var epochRef = dag.findEpochRef(blck, epoch)
if epochRef == nil:
let
ancestor = blck.epochAncestor(epoch)
prevEpochRef = if epoch < 1: nil
else: dag.findEpochRef(blck, epoch - 1)
# Because key stores are additive lists, we can use a newer list whereever an
# older list is expected - all indices in the new list will be valid for the
# old list also
if epochRef.epoch > 0:
var cur = ancestor.blck.epochAncestor(epochRef.epoch - 1)
while cur.slot >= dag.finalizedHead.slot:
let er = dag.findEpochRef(cur.blck, cur.slot.epoch)
if er != nil:
er.validator_key_store = epochRef.validator_key_store
if cur.slot.epoch == 0:
break
cur = cur.blck.epochAncestor(cur.slot.epoch - 1)
epochRef = EpochRef.init(state, cache, prevEpochRef)
if epoch >= dag.finalizedHead.slot.epoch():
# Only cache epoch information for unfinalized blocks - earlier states
# are seldomly used (ie RPC), so no need to cache
# Because we put a cap on the number of epochRefs we store, we want to
# prune the least useful state - for now, we'll assume that to be the
# oldest epochRef we know about.
var
oldest = 0
for x in 0..<dag.epochRefs.len:
let candidate = dag.epochRefs[x]
if candidate[1] == nil:
oldest = x
break
if candidate[1].epoch < dag.epochRefs[oldest][1].epoch:
oldest = x
dag.epochRefs[oldest] = (ancestor.blck, epochRef)
# Because key stores are additive lists, we can use a newer list whereever an
# older list is expected - all indices in the new list will be valid for the
# old list also
if epoch > 0:
var cur = ancestor.blck.epochAncestor(epoch - 1)
while cur.slot >= dag.finalizedHead.slot:
let er = dag.findEpochRef(cur.blck, cur.slot.epoch)
if er != nil:
er.validator_key_store = epochRef.validator_key_store
if cur.slot.epoch == 0:
break
cur = cur.blck.epochAncestor(cur.slot.epoch - 1)
epochRef
proc getEpochRef*(dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochRef =
let epochRef = dag.findEpochRef(blck, epoch)
@ -500,16 +520,7 @@ proc getEpochRef*(dag: ChainDAGRef, blck: BlockRef, epoch: Epoch): EpochRef =
ancestor = blck.epochAncestor(epoch)
dag.withState(dag.epochRefState, ancestor):
let
prevEpochRef = if epoch < 1: nil
else: dag.findEpochRef(blck, epoch - 1)
newEpochRef = EpochRef.init(stateData, cache, prevEpochRef)
if epoch >= dag.finalizedHead.slot.epoch():
# Only cache epoch information for unfinalized blocks - earlier states
# are seldomly used (ie RPC), so no need to cache
dag.addEpochRef(blck, newEpochRef)
newEpochRef
dag.getEpochRef(stateData, cache)
proc getFinalizedEpochRef*(dag: ChainDAGRef): EpochRef =
dag.getEpochRef(dag.finalizedHead.blck, dag.finalizedHead.slot.epoch)

View File

@ -910,6 +910,17 @@ proc onSlotEnd(node: BeaconNode, slot: Slot) {.async.} =
# the database are synced with the filesystem.
node.db.checkpoint()
# When we're not behind schedule, we'll speculatively update the clearance
# state in anticipation of receiving the next block
if node.beaconClock.now() + 500.millis < (slot+1).toBeaconTime():
# This is not a perfect location to be calling advance since the block
# for the current slot may have not arrived yet, specially when running
# a node that is not attesting - there's a small chance we'll call
# advance twice for a block and not at all for the next because of these
# timing effect - this is fine, except for the missed opportunity to
# speculate
node.chainDag.advanceClearanceState()
# -1 is a more useful output than 18446744073709551615 as an indicator of
# no future attestation/proposal known.
template displayInt64(x: Slot): int64 =