nimbus-eth2/beacon_chain/gossip_processing/light_client_processor.nim
Jacek Sieka f70ff38b53
enable styleCheck:usages (#3573)
Some upstream repos still need fixes, but this gets us close enough that
style hints can be enabled by default.

In general, "canonical" spellings are preferred even if they violate
nep-1 - this applies in particular to spec-related stuff like
`genesis_validators_root` which appears throughout the codebase.
2022-04-08 16:22:49 +00:00

281 lines
9.9 KiB
Nim

# beacon_chain
# Copyright (c) 2022 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [Defect].}
import
stew/objects,
chronos, metrics,
../spec/datatypes/altair,
../spec/light_client_sync,
../consensus_object_pools/block_pools_types,
".."/[beacon_clock],
../sszdump
export sszdump
# Light Client Processor
# ------------------------------------------------------------------------------
# The light client processor handles received light client objects
declareHistogram light_client_store_object_duration_seconds,
"storeObject() duration", buckets = [0.25, 0.5, 1, 2, 4, 8, Inf]
type
DidInitializeStoreCallback* =
proc() {.gcsafe, raises: [Defect].}
LightClientProcessor* = object
## This manages the processing of received light client objects
##
## from:
## - Gossip (`OptimisticLightClientUpdate`)
## - SyncManager (`BestLightClientUpdatesByRange`)
## - LightClientManager (`GetLatestLightClientUpdate`,
## `GetOptimisticLightClientUpdate`, `GetLightClientBootstrap`)
##
## are then verified and added to:
## - `LightClientStore`
##
## The processor will also attempt to force-update the light client state
## if no update seems to be available on the network, that is both signed by
## a supermajority of sync committee members and also has a finality proof.
## This logic is triggered if there is no progress for an extended period
## of time, and there are repeated messages indicating that this is the best
## available data on the network during that time period.
# Config
# ----------------------------------------------------------------
dumpEnabled: bool
dumpDirInvalid: string
dumpDirIncoming: string
# Consumer
# ----------------------------------------------------------------
store: ref Option[LightClientStore]
getBeaconTime*: GetBeaconTimeFn
didInitializeStoreCallback: DidInitializeStoreCallback
cfg: RuntimeConfig
genesis_validators_root: Eth2Digest
trustedBlockRoot: Eth2Digest
lastProgressTick: BeaconTime # Moment when last update made progress
lastDuplicateTick: BeaconTime # Moment when last duplicate update received
numDuplicatesSinceProgress: int # Number of duplicates since last progress
const
# These constants have been chosen empirically and are not backed by spec
duplicateRateLimit = chronos.seconds(5) # Rate limit for counting duplicates
duplicateCountDelay = chronos.minutes(15) # Delay to start counting duplicates
minForceUpdateDelay = chronos.minutes(30) # Minimum delay until forced-update
minForceUpdateDuplicates = 100 # Minimum duplicates until forced-update
# Initialization
# ------------------------------------------------------------------------------
proc new*(
T: type LightClientProcessor,
dumpEnabled: bool,
dumpDirInvalid, dumpDirIncoming: string,
cfg: RuntimeConfig,
genesis_validators_root, trustedBlockRoot: Eth2Digest,
store: ref Option[LightClientStore],
getBeaconTime: GetBeaconTimeFn,
didInitializeStoreCallback: DidInitializeStoreCallback = nil
): ref LightClientProcessor =
(ref LightClientProcessor)(
dumpEnabled: dumpEnabled,
dumpDirInvalid: dumpDirInvalid,
dumpDirIncoming: dumpDirIncoming,
store: store,
getBeaconTime: getBeaconTime,
didInitializeStoreCallback: didInitializeStoreCallback,
cfg: cfg,
genesis_validators_root: genesis_validators_root,
trustedBlockRoot: trustedBlockRoot
)
# Storage
# ------------------------------------------------------------------------------
proc dumpInvalidObject(
self: LightClientProcessor,
obj: SomeLightClientObject) =
if self.dumpEnabled:
dump(self.dumpDirInvalid, obj)
proc dumpObject[T](
self: LightClientProcessor,
obj: SomeLightClientObject,
res: Result[T, BlockError]) =
if self.dumpEnabled and res.isErr:
case res.error
of BlockError.Invalid:
self.dumpInvalidObject(obj)
of BlockError.MissingParent:
dump(self.dumpDirIncoming, obj)
else:
discard
proc tryForceUpdate(
self: var LightClientProcessor,
wallTime: BeaconTime) =
## Try to force-update to the next sync committee period.
let
wallSlot = wallTime.slotOrZero()
store = self.store
if store[].isSome:
case store[].get.process_slot_for_light_client_store(wallSlot)
of NoUpdate:
discard
of UpdatedWithoutSupermajority:
warn "Light client force-update without supermajority",
localHeadSlot = store[].get.optimistic_header.slot,
finalized = store[].get.finalized_header
of UpdatedWithoutFinalityProof:
warn "Light client force-update without finality proof",
localHeadSlot = store[].get.optimistic_header.slot,
finalized = store[].get.finalized_header
proc storeObject*(
self: var LightClientProcessor,
src: MsgSource, wallTime: BeaconTime,
obj: SomeLightClientObject): Result[void, BlockError] =
## storeObject is the main entry point for unvalidated light client objects -
## all untrusted objects pass through here. When storing an object, we will
## update the `LightClientStore` accordingly
let
startTick = Moment.now()
wallSlot = wallTime.slotOrZero()
store = self.store
res =
when obj is altair.LightClientBootstrap:
if store[].isSome:
err(BlockError.Duplicate)
else:
let initRes = initialize_light_client_store(
self.trustedBlockRoot, obj)
if initRes.isErr:
err(initRes.error)
else:
store[] = some(initRes.get)
ok()
elif obj is altair.LightClientUpdate:
if store[].isNone:
err(BlockError.MissingParent)
else:
store[].get.process_light_client_update(
obj, wallSlot, self.cfg, self.genesis_validators_root,
allowForceUpdate = false)
elif obj is altair.OptimisticLightClientUpdate:
if store[].isNone:
err(BlockError.MissingParent)
else:
store[].get.process_optimistic_light_client_update(
obj, wallSlot, self.cfg, self.genesis_validators_root)
self.dumpObject(obj, res)
if res.isErr:
when obj is altair.LightClientUpdate:
if store[].isSome and store[].get.best_valid_update.isSome:
# `best_valid_update` gets set when no supermajority / finality proof
# is available. In that case, we will wait for a better update.
# If none is made available within reasonable time, the light client
# is force-updated using the best known data to ensure sync progress.
case res.error
of BlockError.Duplicate:
if wallTime >= self.lastDuplicateTick + duplicateRateLimit:
if self.numDuplicatesSinceProgress < minForceUpdateDuplicates:
let
finalized_period =
store[].get.finalized_header.slot.sync_committee_period
update_period =
obj.get_active_header().slot.sync_committee_period
is_next_sync_committee_known =
not store[].get.next_sync_committee.isZeroMemory
update_can_advance_period =
if is_next_sync_committee_known:
update_period == finalized_period + 1
else:
update_period == finalized_period
if update_can_advance_period:
self.lastDuplicateTick = wallTime
inc self.numDuplicatesSinceProgress
if self.numDuplicatesSinceProgress >= minForceUpdateDuplicates and
wallTime >= self.lastProgressTick + minForceUpdateDelay:
self.tryForceUpdate(wallTime)
self.lastProgressTick = wallTime
self.lastDuplicateTick = wallTime + duplicateCountDelay
self.numDuplicatesSinceProgress = 0
else: discard
return res
when obj is altair.LightClientBootstrap | altair.LightClientUpdate:
self.lastProgressTick = wallTime
self.lastDuplicateTick = wallTime + duplicateCountDelay
self.numDuplicatesSinceProgress = 0
let
storeObjectTick = Moment.now()
storeObjectDur = storeObjectTick - startTick
light_client_store_object_duration_seconds.observe(
storeObjectDur.toFloatSeconds())
let objSlot =
when obj is altair.LightClientBootstrap:
obj.header.slot
else:
obj.attested_header.slot
debug "Light client object processed", kind = typeof(obj).name,
localHeadSlot = store[].get.optimistic_header.slot,
objectSlot = objSlot,
storeObjectDur
when obj is altair.LightClientBootstrap:
if self.didInitializeStoreCallback != nil:
self.didInitializeStoreCallback()
self.didInitializeStoreCallback = nil
res
# Enqueue
# ------------------------------------------------------------------------------
proc addObject*(
self: var LightClientProcessor,
src: MsgSource,
obj: SomeLightClientObject,
resfut: Future[Result[void, BlockError]] = nil) =
## Enqueue a Gossip-validated light client object for verification
# Backpressure:
# Only one object is validated at any time -
# Light client objects are always "fast" to process
# Producers:
# - Gossip (`OptimisticLightClientUpdate`)
# - SyncManager (`BestLightClientUpdatesByRange`)
# - LightClientManager (`GetLatestLightClientUpdate`,
# `GetOptimisticLightClientUpdate`, `GetLightClientBootstrap`)
let
wallTime = self.getBeaconTime()
(afterGenesis, wallSlot) = wallTime.toSlot()
if not afterGenesis:
error "Processing light client object before genesis, clock turned back?"
quit 1
let res = self.storeObject(src, wallTime, obj)
if resFut != nil:
resFut.complete(res)