2022-11-16 23:51:06 +00:00
|
|
|
# Nimbus
|
|
|
|
# Copyright (c) 2021 Status Research & Development GmbH
|
|
|
|
# Licensed under either of
|
|
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0)
|
|
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
|
|
|
# http://opensource.org/licenses/MIT)
|
|
|
|
# at your option. This file may not be copied, modified, or distributed
|
|
|
|
# except according to those terms.
|
|
|
|
|
|
|
|
import
|
|
|
|
std/[math, sequtils],
|
2022-11-25 14:56:42 +00:00
|
|
|
bearssl/rand,
|
2022-11-16 23:51:06 +00:00
|
|
|
chronos,
|
2022-11-25 14:56:42 +00:00
|
|
|
eth/[common, trie/trie_defs],
|
2022-12-12 22:00:24 +00:00
|
|
|
stew/[interval_set, keyed_queue, sorted_set],
|
2022-11-16 23:51:06 +00:00
|
|
|
../../sync_desc,
|
|
|
|
".."/[constants, range_desc, worker_desc],
|
2022-11-25 14:56:42 +00:00
|
|
|
./db/[hexary_error, snapdb_accounts, snapdb_pivot],
|
2022-12-19 21:22:09 +00:00
|
|
|
./pivot/[heal_accounts, heal_storage_slots,
|
|
|
|
range_fetch_accounts, range_fetch_storage_slots],
|
2022-12-12 22:00:24 +00:00
|
|
|
./ticker
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
{.push raises: [Defect].}
|
|
|
|
|
|
|
|
const
|
|
|
|
extraAsserts = false or true
|
|
|
|
## Enable some asserts
|
|
|
|
|
2022-12-12 22:00:24 +00:00
|
|
|
proc pivotAccountsHealingOk*(env: SnapPivotRef;ctx: SnapCtxRef): bool {.gcsafe.}
|
|
|
|
proc pivotMothball*(env: SnapPivotRef) {.gcsafe.}
|
|
|
|
|
2022-11-16 23:51:06 +00:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# Private helpers
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
2022-12-09 13:43:55 +00:00
|
|
|
proc init(
|
|
|
|
batch: SnapRangeBatchRef;
|
|
|
|
stateRoot: Hash256;
|
|
|
|
ctx: SnapCtxRef;
|
|
|
|
) =
|
2022-11-16 23:51:06 +00:00
|
|
|
## Returns a pair of account hash range lists with the full range of hashes
|
|
|
|
## smartly spread across the mutually disjunct interval sets.
|
2022-12-19 21:22:09 +00:00
|
|
|
batch.unprocessed.init() # full range on the first set of the pair
|
2022-11-25 14:56:42 +00:00
|
|
|
batch.processed = NodeTagRangeSet.init()
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
# Initialise accounts range fetch batch, the pair of `fetchAccounts[]`
|
|
|
|
# range sets.
|
|
|
|
if ctx.data.coveredAccounts.isFull:
|
|
|
|
# All of accounts hashes are covered by completed range fetch processes
|
2022-12-19 21:22:09 +00:00
|
|
|
# for all pivot environments. So reset covering and record full-ness level.
|
|
|
|
ctx.data.covAccTimesFull.inc
|
|
|
|
ctx.data.coveredAccounts.clear()
|
|
|
|
|
|
|
|
# Deprioritise already processed ranges by moving it to the second set.
|
|
|
|
for iv in ctx.data.coveredAccounts.increasing:
|
|
|
|
discard batch.unprocessed[0].reduce iv
|
|
|
|
discard batch.unprocessed[1].merge iv
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
when extraAsserts:
|
2022-11-25 14:56:42 +00:00
|
|
|
doAssert batch.unprocessed.verify
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# Public functions: pivot table related
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
proc beforeTopMostlyClean*(pivotTable: var SnapPivotTable) =
|
|
|
|
## Clean up pivot queues of the entry before the top one. The queues are
|
|
|
|
## the pivot data that need most of the memory. This cleaned pivot is not
|
|
|
|
## usable any more after cleaning but might be useful as historic record.
|
|
|
|
let rc = pivotTable.beforeLastValue
|
|
|
|
if rc.isOk:
|
2022-12-12 22:00:24 +00:00
|
|
|
rc.value.pivotMothball
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
proc topNumber*(pivotTable: var SnapPivotTable): BlockNumber =
|
2022-11-25 14:56:42 +00:00
|
|
|
## Return the block number of the top pivot entry, or zero if there is none.
|
2022-11-16 23:51:06 +00:00
|
|
|
let rc = pivotTable.lastValue
|
|
|
|
if rc.isOk:
|
|
|
|
return rc.value.stateHeader.blockNumber
|
|
|
|
|
|
|
|
|
|
|
|
proc update*(
|
2022-12-12 22:00:24 +00:00
|
|
|
pivotTable: var SnapPivotTable; # Pivot table
|
|
|
|
header: BlockHeader; # Header to generate new pivot from
|
|
|
|
ctx: SnapCtxRef; # Some global context
|
|
|
|
reverse = false; # Update from bottom (e.g. for recovery)
|
2022-11-16 23:51:06 +00:00
|
|
|
) =
|
|
|
|
## Activate environment for state root implied by `header` argument. This
|
|
|
|
## function appends a new environment unless there was any not far enough
|
|
|
|
## apart.
|
|
|
|
##
|
|
|
|
## Note that the pivot table is assumed to be sorted by the block numbers of
|
|
|
|
## the pivot header.
|
|
|
|
##
|
2022-12-12 22:00:24 +00:00
|
|
|
# Calculate minimum block distance.
|
|
|
|
let minBlockDistance = block:
|
|
|
|
let rc = pivotTable.lastValue
|
|
|
|
if rc.isOk and rc.value.pivotAccountsHealingOk(ctx):
|
|
|
|
pivotBlockDistanceThrottledPivotChangeMin
|
|
|
|
else:
|
|
|
|
pivotBlockDistanceMin
|
|
|
|
|
2022-11-16 23:51:06 +00:00
|
|
|
# Check whether the new header follows minimum depth requirement. This is
|
|
|
|
# where the queue is assumed to have increasing block numbers.
|
2022-11-25 14:56:42 +00:00
|
|
|
if reverse or
|
|
|
|
pivotTable.topNumber() + pivotBlockDistanceMin < header.blockNumber:
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
# Ok, append a new environment
|
2022-11-25 14:56:42 +00:00
|
|
|
let env = SnapPivotRef(
|
|
|
|
stateHeader: header,
|
|
|
|
fetchAccounts: SnapRangeBatchRef())
|
2022-12-09 13:43:55 +00:00
|
|
|
env.fetchAccounts.init(header.stateRoot, ctx)
|
2022-12-12 22:00:24 +00:00
|
|
|
env.storageAccounts.init()
|
2022-11-25 14:56:42 +00:00
|
|
|
var topEnv = env
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
# Append per-state root environment to LRU queue
|
2022-11-25 14:56:42 +00:00
|
|
|
if reverse:
|
|
|
|
discard pivotTable.prepend(header.stateRoot, env)
|
|
|
|
# Make sure that the LRU table does not grow too big.
|
|
|
|
if max(3, ctx.buddiesMax) < pivotTable.len:
|
|
|
|
# Delete second entry rather thanthe first which might currently
|
|
|
|
# be needed.
|
|
|
|
let rc = pivotTable.secondKey
|
|
|
|
if rc.isOk:
|
|
|
|
pivotTable.del rc.value
|
2022-11-28 09:03:23 +00:00
|
|
|
|
|
|
|
# Update healing threshold for top pivot entry
|
|
|
|
topEnv = pivotTable.lastValue.value
|
|
|
|
|
2022-11-25 14:56:42 +00:00
|
|
|
else:
|
2022-12-12 22:00:24 +00:00
|
|
|
discard pivotTable.lruAppend(
|
|
|
|
header.stateRoot, env, pivotTableLruEntriesMax)
|
2022-11-16 23:51:06 +00:00
|
|
|
|
2022-11-28 09:03:23 +00:00
|
|
|
# Update healing threshold
|
|
|
|
let
|
|
|
|
slots = max(0, healAccountsPivotTriggerNMax - pivotTable.len)
|
|
|
|
delta = slots.float * healAccountsPivotTriggerWeight
|
|
|
|
topEnv.healThresh = healAccountsPivotTriggerMinFactor + delta
|
|
|
|
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
proc tickerStats*(
|
2022-12-12 22:00:24 +00:00
|
|
|
pivotTable: var SnapPivotTable; # Pivot table
|
|
|
|
ctx: SnapCtxRef; # Some global context
|
2022-11-16 23:51:06 +00:00
|
|
|
): TickerStatsUpdater =
|
|
|
|
## This function returns a function of type `TickerStatsUpdater` that prints
|
|
|
|
## out pivot table statitics. The returned fuction is supposed to drive
|
|
|
|
## ticker` module.
|
|
|
|
proc meanStdDev(sum, sqSum: float; length: int): (float,float) =
|
|
|
|
if 0 < length:
|
|
|
|
result[0] = sum / length.float
|
|
|
|
result[1] = sqrt(sqSum / length.float - result[0] * result[0])
|
|
|
|
|
|
|
|
result = proc: TickerStats =
|
|
|
|
var
|
|
|
|
aSum, aSqSum, uSum, uSqSum, sSum, sSqSum: float
|
|
|
|
count = 0
|
|
|
|
for kvp in ctx.data.pivotTable.nextPairs:
|
|
|
|
|
|
|
|
# Accounts mean & variance
|
|
|
|
let aLen = kvp.data.nAccounts.float
|
|
|
|
if 0 < aLen:
|
|
|
|
count.inc
|
|
|
|
aSum += aLen
|
|
|
|
aSqSum += aLen * aLen
|
|
|
|
|
|
|
|
# Fill utilisation mean & variance
|
2022-11-25 14:56:42 +00:00
|
|
|
let fill = kvp.data.fetchAccounts.processed.fullFactor
|
2022-11-16 23:51:06 +00:00
|
|
|
uSum += fill
|
|
|
|
uSqSum += fill * fill
|
|
|
|
|
|
|
|
let sLen = kvp.data.nSlotLists.float
|
|
|
|
sSum += sLen
|
|
|
|
sSqSum += sLen * sLen
|
|
|
|
let
|
|
|
|
env = ctx.data.pivotTable.lastValue.get(otherwise = nil)
|
2022-12-19 21:22:09 +00:00
|
|
|
accCoverage = (ctx.data.coveredAccounts.fullFactor +
|
|
|
|
ctx.data.covAccTimesFull.float)
|
2022-11-16 23:51:06 +00:00
|
|
|
accFill = meanStdDev(uSum, uSqSum, count)
|
|
|
|
var
|
|
|
|
pivotBlock = none(BlockNumber)
|
|
|
|
stoQuLen = none(int)
|
2022-12-20 15:38:57 +00:00
|
|
|
procChunks = 0
|
2022-11-16 23:51:06 +00:00
|
|
|
if not env.isNil:
|
|
|
|
pivotBlock = some(env.stateHeader.blockNumber)
|
|
|
|
stoQuLen = some(env.fetchStorageFull.len + env.fetchStoragePart.len)
|
2022-12-20 15:38:57 +00:00
|
|
|
procChunks = env.fetchAccounts.processed.chunks
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
TickerStats(
|
|
|
|
pivotBlock: pivotBlock,
|
|
|
|
nQueues: ctx.data.pivotTable.len,
|
|
|
|
nAccounts: meanStdDev(aSum, aSqSum, count),
|
|
|
|
nSlotLists: meanStdDev(sSum, sSqSum, count),
|
|
|
|
accountsFill: (accFill[0], accFill[1], accCoverage),
|
2022-12-20 15:38:57 +00:00
|
|
|
nAccountStats: procChunks,
|
2022-11-16 23:51:06 +00:00
|
|
|
nStorageQueue: stoQuLen)
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# Public functions: particular pivot
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
2022-12-12 22:00:24 +00:00
|
|
|
proc pivotMothball*(env: SnapPivotRef) =
|
|
|
|
## Clean up most of this argument `env` pivot record and mark it `archived`.
|
|
|
|
## Note that archived pivots will be checked for swapping in already known
|
|
|
|
## accounts and storage slots.
|
|
|
|
env.fetchAccounts.unprocessed.init()
|
|
|
|
|
|
|
|
# Simplify storage slots queues by resolving partial slots into full list
|
|
|
|
for kvp in env.fetchStoragePart.nextPairs:
|
|
|
|
discard env.fetchStorageFull.append(
|
|
|
|
kvp.key, SnapSlotsQueueItemRef(acckey: kvp.data.accKey))
|
|
|
|
env.fetchStoragePart.clear()
|
|
|
|
|
|
|
|
# Provide index into `fetchStorageFull`
|
|
|
|
env.storageAccounts.clear()
|
|
|
|
for kvp in env.fetchStorageFull.nextPairs:
|
|
|
|
let rc = env.storageAccounts.insert(kvp.data.accKey.to(NodeTag))
|
|
|
|
# Note that `rc.isErr` should not exist as accKey => storageRoot
|
|
|
|
if rc.isOk:
|
|
|
|
rc.value.data = kvp.key
|
|
|
|
|
|
|
|
# Finally, mark that node `archived`
|
|
|
|
env.archived = true
|
|
|
|
|
|
|
|
|
2022-11-28 09:03:23 +00:00
|
|
|
proc pivotAccountsHealingOk*(
|
2022-12-12 22:00:24 +00:00
|
|
|
env: SnapPivotRef; # Current pivot environment
|
|
|
|
ctx: SnapCtxRef; # Some global context
|
2022-11-28 09:03:23 +00:00
|
|
|
): bool =
|
|
|
|
## Returns `true` if accounts healing is enabled for this pivot.
|
|
|
|
##
|
2022-12-19 21:22:09 +00:00
|
|
|
# Only start accounts healing if there is some completion level, already.
|
|
|
|
#
|
|
|
|
# We check against the global coverage factor, i.e. a measure for how much
|
|
|
|
# of the total of all accounts have been processed. Even if the hexary trie
|
|
|
|
# database for the current pivot state root is sparsely filled, there is a
|
|
|
|
# good chance that it can inherit some unchanged sub-trie from an earlier
|
|
|
|
# pivot state root download. The healing process then works like sort of
|
|
|
|
# glue.
|
|
|
|
if healAccountsCoverageTrigger <= ctx.pivotAccountsCoverage():
|
|
|
|
if env.healThresh <= env.fetchAccounts.processed.fullFactor:
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
proc execSnapSyncAction*(
|
|
|
|
env: SnapPivotRef; # Current pivot environment
|
|
|
|
buddy: SnapBuddyRef; # Worker peer
|
|
|
|
) {.async.} =
|
|
|
|
## Execute a synchronisation run.
|
|
|
|
let
|
|
|
|
ctx = buddy.ctx
|
|
|
|
|
|
|
|
block:
|
|
|
|
# Clean up storage slots queue first it it becomes too large
|
|
|
|
let nStoQu = env.fetchStorageFull.len + env.fetchStoragePart.len
|
|
|
|
if snapStorageSlotsQuPrioThresh < nStoQu:
|
|
|
|
await buddy.rangeFetchStorageSlots(env)
|
|
|
|
if buddy.ctrl.stopped or env.archived:
|
|
|
|
return
|
|
|
|
|
|
|
|
if not env.fetchAccounts.processed.isFull:
|
|
|
|
await buddy.rangeFetchAccounts(env)
|
|
|
|
|
|
|
|
# Run at least one round fetching storage slosts even if the `archived`
|
|
|
|
# flag is set in order to keep the batch queue small.
|
|
|
|
if not buddy.ctrl.stopped:
|
|
|
|
await buddy.rangeFetchStorageSlots(env)
|
|
|
|
|
|
|
|
if buddy.ctrl.stopped or env.archived:
|
|
|
|
return
|
|
|
|
|
|
|
|
if env.pivotAccountsHealingOk(ctx):
|
|
|
|
await buddy.healAccounts(env)
|
|
|
|
if buddy.ctrl.stopped or env.archived:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Some additional storage slots might have been popped up
|
|
|
|
await buddy.rangeFetchStorageSlots(env)
|
|
|
|
if buddy.ctrl.stopped or env.archived:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Don't bother with storage slots healing before accounts healing takes
|
|
|
|
# place. This saves communication bandwidth. The pivot might change soon,
|
|
|
|
# anyway.
|
|
|
|
if env.pivotAccountsHealingOk(ctx):
|
|
|
|
await buddy.healStorageSlots(env)
|
2022-11-28 09:03:23 +00:00
|
|
|
|
|
|
|
|
2022-11-25 14:56:42 +00:00
|
|
|
proc saveCheckpoint*(
|
2022-12-12 22:00:24 +00:00
|
|
|
env: SnapPivotRef; # Current pivot environment
|
|
|
|
ctx: SnapCtxRef; # Some global context
|
2022-11-28 09:03:23 +00:00
|
|
|
): Result[int,HexaryError] =
|
2022-11-16 23:51:06 +00:00
|
|
|
## Save current sync admin data. On success, the size of the data record
|
|
|
|
## saved is returned (e.g. for logging.)
|
2022-11-25 14:56:42 +00:00
|
|
|
##
|
|
|
|
let
|
|
|
|
fa = env.fetchAccounts
|
|
|
|
nStoQu = env.fetchStorageFull.len + env.fetchStoragePart.len
|
|
|
|
|
|
|
|
if snapAccountsSaveProcessedChunksMax < fa.processed.chunks:
|
|
|
|
return err(TooManyProcessedChunks)
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
if snapAccountsSaveStorageSlotsMax < nStoQu:
|
|
|
|
return err(TooManySlotAccounts)
|
|
|
|
|
2022-11-25 14:56:42 +00:00
|
|
|
ctx.data.snapDb.savePivot SnapDbPivotRegistry(
|
|
|
|
header: env.stateHeader,
|
|
|
|
nAccounts: env.nAccounts,
|
|
|
|
nSlotLists: env.nSlotLists,
|
|
|
|
processed: toSeq(env.fetchAccounts.processed.increasing)
|
|
|
|
.mapIt((it.minPt,it.maxPt)),
|
|
|
|
slotAccounts: (toSeq(env.fetchStorageFull.nextKeys) &
|
|
|
|
toSeq(env.fetchStoragePart.nextKeys)).mapIt(it.to(NodeKey)))
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
|
2022-11-25 14:56:42 +00:00
|
|
|
proc recoverPivotFromCheckpoint*(
|
2022-12-12 22:00:24 +00:00
|
|
|
env: SnapPivotRef; # Current pivot environment
|
|
|
|
ctx: SnapCtxRef; # Global context (containing save state)
|
|
|
|
topLevel: bool; # Full data set on top level only
|
2022-11-25 14:56:42 +00:00
|
|
|
) =
|
|
|
|
## Recover some pivot variables and global list `coveredAccounts` from
|
|
|
|
## checkpoint data. If the argument `toplevel` is set `true`, also the
|
|
|
|
## `processed`, `unprocessed`, and the `fetchStorageFull` lists are
|
|
|
|
## initialised.
|
|
|
|
##
|
|
|
|
let recov = ctx.data.recovery
|
|
|
|
if recov.isNil:
|
|
|
|
return
|
|
|
|
|
|
|
|
env.nAccounts = recov.state.nAccounts
|
|
|
|
env.nSlotLists = recov.state.nSlotLists
|
|
|
|
|
|
|
|
# Import processed interval
|
|
|
|
for (minPt,maxPt) in recov.state.processed:
|
|
|
|
if topLevel:
|
|
|
|
env.fetchAccounts.unprocessed.reduce(minPt, maxPt)
|
2022-12-12 22:00:24 +00:00
|
|
|
discard env.fetchAccounts.processed.merge(minPt, maxPt)
|
2022-11-25 14:56:42 +00:00
|
|
|
discard ctx.data.coveredAccounts.merge(minPt, maxPt)
|
|
|
|
|
|
|
|
# Handle storage slots
|
2022-12-12 22:00:24 +00:00
|
|
|
let stateRoot = recov.state.header.stateRoot
|
|
|
|
for w in recov.state.slotAccounts:
|
2022-12-19 21:22:09 +00:00
|
|
|
let pt = NodeTagRange.new(w.to(NodeTag),w.to(NodeTag)) # => `pt.len == 1`
|
2022-12-12 22:00:24 +00:00
|
|
|
|
|
|
|
if 0 < env.fetchAccounts.processed.covered(pt):
|
|
|
|
# Ignoring slots that have accounts to be downloaded, anyway
|
|
|
|
let rc = ctx.data.snapDb.getAccountsData(stateRoot, w)
|
|
|
|
if rc.isErr:
|
|
|
|
# Oops, how did that account get lost?
|
|
|
|
discard env.fetchAccounts.processed.reduce pt
|
|
|
|
env.fetchAccounts.unprocessed.merge pt
|
|
|
|
elif rc.value.storageRoot != emptyRlpHash:
|
|
|
|
env.fetchStorageFull.merge AccountSlotsHeader(
|
|
|
|
accKey: w,
|
|
|
|
storageRoot: rc.value.storageRoot)
|
|
|
|
|
|
|
|
# Handle mothballed pivots for swapping in (see `pivotMothball()`)
|
|
|
|
if not topLevel:
|
|
|
|
for kvp in env.fetchStorageFull.nextPairs:
|
|
|
|
let rc = env.storageAccounts.insert(kvp.data.accKey.to(NodeTag))
|
|
|
|
if rc.isOk:
|
|
|
|
rc.value.data = kvp.key
|
|
|
|
env.archived = true
|
2022-11-16 23:51:06 +00:00
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# End
|
|
|
|
# ------------------------------------------------------------------------------
|