mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-02-03 07:45:18 +00:00
fe3a6d67c6
* Clean up some function prototypes why: Simplify polymorphic prototype variances for easier maintenance. * Fix fringe condition crash when importing bogus RLP node why: Accessing non-list RLP entry as a list causes `Defect` * Fix left boundary proof at range extractor why: Was insufficient. The main problem was that there was no unit test for the validity of the generated left boundary. * Handle incomplete left boundary proofs early why: Attempt to do it later leads to overly complex code in order to prevent looping when the same peer repeats to send the same incomplete proof. Contrary, gaps in the leaf sequence can be handled gracefully with registering the gaps * Implement a manual pivot setup mechanism for snap sync why: For a test scenario it is convenient to set the pivot to something lower than the beacon header from the consensus layer. This does not need rely on any RPC mechanism. details: The file containing the pivot specs is specified by the `--sync-ctrl-file` option. It is regularly parsed for updates. * Fix calculation error why: Prevent from calculating negative square root
423 lines
14 KiB
Nim
423 lines
14 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2018-2021 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:[].}
|
|
|
|
import
|
|
std/[options],
|
|
chronicles,
|
|
chronos,
|
|
eth/[common, p2p],
|
|
".."/[protocol, sync_desc],
|
|
../misc/[best_pivot, block_queue, sync_ctrl],
|
|
"."/[ticker, worker_desc]
|
|
|
|
logScope:
|
|
topics = "full-buddy"
|
|
|
|
const
|
|
extraTraceMessages = false # or true
|
|
## Enabled additional logging noise
|
|
|
|
FirstPivotSeenTimeout = 3.minutes
|
|
## Turn on relaxed pivot negotiation after some waiting time when there
|
|
## was a `peer` seen but was rejected. This covers a rare event. Typically
|
|
## useless peers do not appear ready for negotiation.
|
|
|
|
FirstPivotAcceptedTimeout = 50.seconds
|
|
## Turn on relaxed pivot negotiation after some waiting time when there
|
|
## was a `peer` accepted but no second one yet.
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Private helpers
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc pp(n: BlockNumber): string =
|
|
## Dedicated pretty printer (`$` is defined elsewhere using `UInt256`)
|
|
if n == high(BlockNumber): "high" else:"#" & $n
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Private functions
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc topUsedNumber(
|
|
ctx: FullCtxRef;
|
|
backBlocks = maxHeadersFetch;
|
|
): Result[BlockNumber,void] =
|
|
var
|
|
top = 0.toBlockNumber
|
|
try:
|
|
let
|
|
bestNumber = ctx.chain.db.getCanonicalHead().blockNumber
|
|
nBackBlocks = backBlocks.toBlockNumber
|
|
# Initialise before best block number
|
|
if nBackBlocks < bestNumber:
|
|
top = bestNumber - nBackBlocks
|
|
except CatchableError as e:
|
|
error "Best block header problem", backBlocks, error=($e.name), msg=e.msg
|
|
return err()
|
|
|
|
ok(top)
|
|
|
|
|
|
proc tickerUpdater(ctx: FullCtxRef): TickerStatsUpdater =
|
|
result = proc: TickerStats =
|
|
var stats: BlockQueueStats
|
|
ctx.pool.bCtx.blockQueueStats(stats)
|
|
|
|
let suspended =
|
|
0 < ctx.pool.suspendAt and ctx.pool.suspendAt < stats.topAccepted
|
|
|
|
TickerStats(
|
|
topPersistent: stats.topAccepted,
|
|
nextStaged: stats.nextStaged,
|
|
nextUnprocessed: stats.nextUnprocessed,
|
|
nStagedQueue: stats.nStagedQueue,
|
|
suspended: suspended,
|
|
reOrg: stats.reOrg)
|
|
|
|
|
|
proc processStaged(buddy: FullBuddyRef): bool =
|
|
## Fetch a work item from the `staged` queue an process it to be
|
|
## stored on the persistent block chain.
|
|
let
|
|
ctx {.used.} = buddy.ctx
|
|
peer = buddy.peer
|
|
chainDb = buddy.ctx.chain.db
|
|
chain = buddy.ctx.chain
|
|
bq = buddy.only.bQueue
|
|
|
|
# Get a work item, a list of headers + bodies
|
|
wi = block:
|
|
let rc = bq.blockQueueFetchStaged()
|
|
if rc.isErr:
|
|
return false
|
|
rc.value
|
|
|
|
#startNumber = wi.headers[0].blockNumber -- unused
|
|
|
|
# Store in persistent database
|
|
try:
|
|
if chain.persistBlocks(wi.headers, wi.bodies) == ValidationResult.OK:
|
|
bq.blockQueueAccept(wi)
|
|
return true
|
|
except CatchableError as e:
|
|
error "Storing persistent blocks failed", peer, range=($wi.blocks),
|
|
error = $e.name, msg = e.msg
|
|
|
|
# Something went wrong. Recycle work item (needs to be re-fetched, anyway)
|
|
let
|
|
parentHash = wi.headers[0].parentHash
|
|
try:
|
|
# Check whether hash of the first block is consistent
|
|
var parent: BlockHeader
|
|
if chainDb.getBlockHeader(parentHash, parent):
|
|
# First block parent is ok, so there might be other problems. Re-fetch
|
|
# the blocks from another peer.
|
|
trace "Storing persistent blocks failed", peer, range=($wi.blocks)
|
|
bq.blockQueueRecycle(wi)
|
|
buddy.ctrl.zombie = true
|
|
return false
|
|
except CatchableError as e:
|
|
error "Failed to access parent blocks", peer,
|
|
blockNumber=wi.headers[0].blockNumber.pp, error=($e.name), msg=e.msg
|
|
|
|
# Parent block header problem, so we might be in the middle of a re-org.
|
|
# Set single mode backtrack following the offending parent hash.
|
|
bq.blockQueueBacktrackFrom(wi)
|
|
buddy.ctrl.multiOk = false
|
|
|
|
if wi.topHash.isNone:
|
|
# Assuming that currently staged entries are on the wrong branch
|
|
bq.blockQueueRecycleStaged()
|
|
notice "Starting chain re-org backtrack work item", peer, range=($wi.blocks)
|
|
else:
|
|
# Leave that block range in the staged list
|
|
trace "Resuming chain re-org backtrack work item", peer, range=($wi.blocks)
|
|
discard
|
|
|
|
return false
|
|
|
|
|
|
proc suspendDownload(buddy: FullBuddyRef): bool =
|
|
## Check whether downloading should be suspended
|
|
let ctx = buddy.ctx
|
|
if ctx.exCtrlFile.isSome:
|
|
let rc = ctx.exCtrlFile.syncCtrlBlockNumberFromFile
|
|
if rc.isOk:
|
|
ctx.pool.suspendAt = rc.value
|
|
if 0 < ctx.pool.suspendAt:
|
|
return ctx.pool.suspendAt < buddy.only.bQueue.topAccepted
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public start/stop and admin functions
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc setup*(ctx: FullCtxRef; tickerOK: bool): bool =
|
|
## Global set up
|
|
ctx.pool.pivot = BestPivotCtxRef.init(ctx.pool.rng)
|
|
let rc = ctx.topUsedNumber(backBlocks = 0)
|
|
if rc.isErr:
|
|
ctx.pool.bCtx = BlockQueueCtxRef.init()
|
|
return false
|
|
ctx.pool.bCtx = BlockQueueCtxRef.init(rc.value + 1)
|
|
if tickerOK:
|
|
ctx.pool.ticker = TickerRef.init(ctx.tickerUpdater)
|
|
else:
|
|
debug "Ticker is disabled"
|
|
|
|
if ctx.exCtrlFile.isSome:
|
|
warn "Full sync accepts suspension request block number",
|
|
syncCtrlFile=ctx.exCtrlFile.get
|
|
true
|
|
|
|
proc release*(ctx: FullCtxRef) =
|
|
## Global clean up
|
|
ctx.pool.pivot = nil
|
|
if not ctx.pool.ticker.isNil:
|
|
ctx.pool.ticker.stop()
|
|
|
|
proc start*(buddy: FullBuddyRef): bool =
|
|
## Initialise worker peer
|
|
let
|
|
ctx = buddy.ctx
|
|
peer = buddy.peer
|
|
if peer.supports(protocol.eth) and
|
|
peer.state(protocol.eth).initialized:
|
|
if not ctx.pool.ticker.isNil:
|
|
ctx.pool.ticker.startBuddy()
|
|
buddy.only.pivot =
|
|
BestPivotWorkerRef.init(ctx.pool.pivot, buddy.ctrl, buddy.peer)
|
|
buddy.only.bQueue = BlockQueueWorkerRef.init(
|
|
ctx.pool.bCtx, buddy.ctrl, peer)
|
|
return true
|
|
|
|
proc stop*(buddy: FullBuddyRef) =
|
|
## Clean up this peer
|
|
buddy.ctrl.stopped = true
|
|
buddy.only.pivot.clear()
|
|
if not buddy.ctx.pool.ticker.isNil:
|
|
buddy.ctx.pool.ticker.stopBuddy()
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public functions
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc runDaemon*(ctx: FullCtxRef) {.async.} =
|
|
## Global background job that will be re-started as long as the variable
|
|
## `ctx.daemon` is set `true`. If that job was stopped due to re-setting
|
|
## `ctx.daemon` to `false`, it will be restarted next after it was reset
|
|
## as `true` not before there is some activity on the `runPool()`,
|
|
## `runSingle()`, or `runMulti()` functions.
|
|
##
|
|
case ctx.pool.pivotState:
|
|
of FirstPivotSeen:
|
|
let elapsed = Moment.now() - ctx.pool.pivotStamp
|
|
if FirstPivotSeenTimeout < elapsed:
|
|
# Switch to single peer pivot negotiation
|
|
ctx.pool.pivot.pivotRelaxedMode(enable = true)
|
|
|
|
# Currently no need for other monitor tasks
|
|
ctx.daemon = false
|
|
|
|
when extraTraceMessages:
|
|
trace "First seen pivot timeout", elapsed,
|
|
pivotState=ctx.pool.pivotState
|
|
return
|
|
# Otherwise delay for some time
|
|
|
|
of FirstPivotAccepted:
|
|
let elapsed = Moment.now() - ctx.pool.pivotStamp
|
|
if FirstPivotAcceptedTimeout < elapsed:
|
|
# Switch to single peer pivot negotiation
|
|
ctx.pool.pivot.pivotRelaxedMode(enable = true)
|
|
|
|
# Use currents pivot next time `runSingle()` is visited. This bent is
|
|
# necessary as there must be a peer initialising and syncing blocks. But
|
|
# this daemon has no peer assigned.
|
|
ctx.pool.pivotState = FirstPivotUseRegardless
|
|
|
|
# Currently no need for other monitor tasks
|
|
ctx.daemon = false
|
|
|
|
when extraTraceMessages:
|
|
trace "First accepted pivot timeout", elapsed,
|
|
pivotState=ctx.pool.pivotState
|
|
return
|
|
# Otherwise delay for some time
|
|
|
|
else:
|
|
# Currently no need for other monitior tasks
|
|
ctx.daemon = false
|
|
return
|
|
|
|
# Without waiting, this function repeats every 50ms (as set with the constant
|
|
# `sync_sched.execLoopTimeElapsedMin`.) Larger waiting time cleans up logging.
|
|
await sleepAsync 300.milliseconds
|
|
|
|
|
|
proc runSingle*(buddy: FullBuddyRef) {.async.} =
|
|
## This peer worker is invoked if the peer-local flag `buddy.ctrl.multiOk`
|
|
## is set `false` which is the default mode. This flag is updated by the
|
|
## worker when deemed appropriate.
|
|
## * For all workers, there can be only one `runSingle()` function active
|
|
## simultaneously for all worker peers.
|
|
## * There will be no `runMulti()` function active for the same worker peer
|
|
## simultaneously
|
|
## * There will be no `runPool()` iterator active simultaneously.
|
|
##
|
|
## Note that this function runs in `async` mode.
|
|
##
|
|
let
|
|
ctx = buddy.ctx
|
|
peer {.used.} = buddy.peer
|
|
bq = buddy.only.bQueue
|
|
pv = buddy.only.pivot
|
|
|
|
when extraTraceMessages:
|
|
trace "Single mode begin", peer, pivotState=ctx.pool.pivotState
|
|
|
|
case ctx.pool.pivotState:
|
|
of PivotStateInitial:
|
|
# Set initial state on first encounter
|
|
ctx.pool.pivotState = FirstPivotSeen
|
|
ctx.pool.pivotStamp = Moment.now()
|
|
ctx.daemon = true # Start monitor
|
|
|
|
of FirstPivotSeen, FirstPivotAccepted:
|
|
discard
|
|
|
|
of FirstPivotUseRegardless:
|
|
# Magic case when we accept anything under the sun
|
|
let rc = pv.pivotHeader(relaxedMode=true)
|
|
if rc.isOK:
|
|
# Update/activate `bestNumber` from the pivot header
|
|
bq.bestNumber = some(rc.value.blockNumber)
|
|
ctx.pool.pivotState = PivotRunMode
|
|
buddy.ctrl.multiOk = true
|
|
trace "Single pivot accepted", peer, pivot=('#' & $bq.bestNumber.get)
|
|
return # stop logging, otherwise unconditional return for this case
|
|
|
|
when extraTraceMessages:
|
|
trace "Single mode stopped", peer, pivotState=ctx.pool.pivotState
|
|
return # unconditional return for this case
|
|
|
|
of PivotRunMode:
|
|
# Sync backtrack runs in single mode
|
|
if bq.blockQueueBacktrackOk:
|
|
let rc = await bq.blockQueueBacktrackWorker()
|
|
if rc.isOk:
|
|
# Update persistent database (may reset `multiOk`)
|
|
buddy.ctrl.multiOk = true
|
|
while buddy.processStaged() and not buddy.ctrl.stopped:
|
|
# Allow thread switch as `persistBlocks()` might be slow
|
|
await sleepAsync(10.milliseconds)
|
|
when extraTraceMessages:
|
|
trace "Single backtrack mode done", peer
|
|
return
|
|
|
|
buddy.ctrl.zombie = true
|
|
|
|
when extraTraceMessages:
|
|
trace "Single backtrack mode stopped", peer
|
|
return
|
|
# End case()
|
|
|
|
# Negotiate in order to derive the pivot header from this `peer`. This code
|
|
# location here is reached when there was no compelling reason for the
|
|
# `case()` handler to process and `return`.
|
|
if await pv.pivotNegotiate(buddy.only.bQueue.bestNumber):
|
|
# Update/activate `bestNumber` from the pivot header
|
|
bq.bestNumber = some(pv.pivotHeader.value.blockNumber)
|
|
ctx.pool.pivotState = PivotRunMode
|
|
buddy.ctrl.multiOk = true
|
|
trace "Pivot accepted", peer, pivot=('#' & $bq.bestNumber.get)
|
|
return
|
|
|
|
if buddy.ctrl.stopped:
|
|
when extraTraceMessages:
|
|
trace "Single mode stopped", peer, pivotState=ctx.pool.pivotState
|
|
return # done with this buddy
|
|
|
|
var napping = 2.seconds
|
|
case ctx.pool.pivotState:
|
|
of FirstPivotSeen:
|
|
# Possible state transition
|
|
if pv.pivotHeader(relaxedMode=true).isOk:
|
|
ctx.pool.pivotState = FirstPivotAccepted
|
|
ctx.pool.pivotStamp = Moment.now()
|
|
napping = 300.milliseconds
|
|
of FirstPivotAccepted:
|
|
napping = 300.milliseconds
|
|
else:
|
|
discard
|
|
|
|
when extraTraceMessages:
|
|
trace "Single mode end", peer, pivotState=ctx.pool.pivotState, napping
|
|
|
|
# Without waiting, this function repeats every 50ms (as set with the constant
|
|
# `sync_sched.execLoopTimeElapsedMin`.)
|
|
await sleepAsync napping
|
|
|
|
|
|
proc runPool*(buddy: FullBuddyRef; last: bool): bool =
|
|
## Once started, the function `runPool()` is called for all worker peers in
|
|
## sequence as the body of an iteration as long as the function returns
|
|
## `false`. There will be no other worker peer functions activated
|
|
## simultaneously.
|
|
##
|
|
## This procedure is started if the global flag `buddy.ctx.poolMode` is set
|
|
## `true` (default is `false`.) It is the responsibility of the `runPool()`
|
|
## instance to reset the flag `buddy.ctx.poolMode`, typically at the first
|
|
## peer instance.
|
|
##
|
|
## The argument `last` is set `true` if the last entry is reached.
|
|
##
|
|
## Note that this function does not run in `async` mode.
|
|
##
|
|
# Mind the gap, fill in if necessary (function is peer independent)
|
|
buddy.only.bQueue.blockQueueGrout()
|
|
|
|
# Stop after running once regardless of peer
|
|
buddy.ctx.poolMode = false
|
|
true
|
|
|
|
proc runMulti*(buddy: FullBuddyRef) {.async.} =
|
|
## This peer worker is invoked if the `buddy.ctrl.multiOk` flag is set
|
|
## `true` which is typically done after finishing `runSingle()`. This
|
|
## instance can be simultaneously active for all peer workers.
|
|
##
|
|
let
|
|
ctx = buddy.ctx
|
|
bq = buddy.only.bQueue
|
|
|
|
if buddy.suspendDownload:
|
|
# Sleep for a while, then leave
|
|
await sleepAsync(10.seconds)
|
|
return
|
|
|
|
# Fetch work item
|
|
let rc = await bq.blockQueueWorker()
|
|
if rc.isErr:
|
|
if rc.error == StagedQueueOverflow:
|
|
# Mind the gap: Turn on pool mode if there are too may staged items.
|
|
ctx.poolMode = true
|
|
else:
|
|
return
|
|
|
|
# Update persistent database
|
|
while buddy.processStaged() and not buddy.ctrl.stopped:
|
|
# Allow thread switch as `persistBlocks()` might be slow
|
|
await sleepAsync(10.milliseconds)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# End
|
|
# ------------------------------------------------------------------------------
|