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:
parent
aa6177814e
commit
df7bc87af5
|
@ -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!
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in New Issue