2021-12-08 21:15:29 +00:00
|
|
|
|
# beacon_chain
|
2023-01-20 14:14:37 +00:00
|
|
|
|
# Copyright (c) 2018-2023 Status Research & Development GmbH
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# 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.
|
|
|
|
|
|
2023-01-20 14:14:37 +00:00
|
|
|
|
{.push raises: [].}
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
2023-03-31 20:46:47 +00:00
|
|
|
|
import std/[heapqueue, tables, strutils, sequtils, math]
|
2022-03-03 08:05:33 +00:00
|
|
|
|
import stew/[results, base10], chronos, chronicles
|
2021-12-08 21:15:29 +00:00
|
|
|
|
import
|
2022-01-18 13:36:52 +00:00
|
|
|
|
../spec/datatypes/[base, phase0, altair],
|
2021-12-08 21:15:29 +00:00
|
|
|
|
../spec/[helpers, forks],
|
|
|
|
|
../networking/[peer_pool, eth2_network],
|
|
|
|
|
../gossip_processing/block_processor,
|
2022-01-26 12:20:08 +00:00
|
|
|
|
../consensus_object_pools/block_pools_types
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
export base, phase0, altair, merge, chronos, chronicles, results,
|
2022-01-26 12:20:08 +00:00
|
|
|
|
block_pools_types, helpers
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
logScope:
|
|
|
|
|
topics = "syncqueue"
|
|
|
|
|
|
|
|
|
|
type
|
2023-08-25 09:29:07 +00:00
|
|
|
|
GetSlotCallback* = proc(): Slot {.gcsafe, raises: [].}
|
|
|
|
|
ProcessingCallback* = proc() {.gcsafe, raises: [].}
|
2023-04-18 00:12:57 +00:00
|
|
|
|
BlockVerifier* = proc(signedBlock: ForkedSignedBeaconBlock,
|
2023-05-19 16:25:11 +00:00
|
|
|
|
blobs: Opt[BlobSidecars], maybeFinalized: bool):
|
2023-08-25 09:29:07 +00:00
|
|
|
|
Future[Result[void, VerifierError]] {.gcsafe, raises: [].}
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
SyncQueueKind* {.pure.} = enum
|
|
|
|
|
Forward, Backward
|
|
|
|
|
|
|
|
|
|
SyncRequest*[T] = object
|
2022-03-03 08:05:33 +00:00
|
|
|
|
kind*: SyncQueueKind
|
2021-12-08 21:15:29 +00:00
|
|
|
|
index*: uint64
|
|
|
|
|
slot*: Slot
|
|
|
|
|
count*: uint64
|
|
|
|
|
item*: T
|
|
|
|
|
|
|
|
|
|
SyncResult*[T] = object
|
|
|
|
|
request*: SyncRequest[T]
|
2022-02-07 17:20:10 +00:00
|
|
|
|
data*: seq[ref ForkedSignedBeaconBlock]
|
2023-03-07 20:19:17 +00:00
|
|
|
|
blobs*: Opt[seq[BlobSidecars]]
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
2022-09-19 09:37:42 +00:00
|
|
|
|
GapItem*[T] = object
|
|
|
|
|
start*: Slot
|
|
|
|
|
finish*: Slot
|
|
|
|
|
item*: T
|
|
|
|
|
|
2021-12-16 14:57:16 +00:00
|
|
|
|
SyncWaiter* = ref object
|
|
|
|
|
future: Future[void]
|
|
|
|
|
reset: bool
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
RewindPoint = object
|
|
|
|
|
failSlot: Slot
|
|
|
|
|
epochCount: uint64
|
|
|
|
|
|
|
|
|
|
SyncQueue*[T] = ref object
|
|
|
|
|
kind*: SyncQueueKind
|
|
|
|
|
inpSlot*: Slot
|
|
|
|
|
outSlot*: Slot
|
|
|
|
|
startSlot*: Slot
|
|
|
|
|
finalSlot*: Slot
|
|
|
|
|
chunkSize*: uint64
|
|
|
|
|
queueSize*: int
|
|
|
|
|
counter*: uint64
|
|
|
|
|
pending*: Table[uint64, SyncRequest[T]]
|
2022-09-19 09:37:42 +00:00
|
|
|
|
gapList*: seq[GapItem[T]]
|
2021-12-16 14:57:16 +00:00
|
|
|
|
waiters: seq[SyncWaiter]
|
2021-12-08 21:15:29 +00:00
|
|
|
|
getSafeSlot*: GetSlotCallback
|
|
|
|
|
debtsQueue: HeapQueue[SyncRequest[T]]
|
|
|
|
|
debtsCount: uint64
|
|
|
|
|
readyQueue: HeapQueue[SyncResult[T]]
|
|
|
|
|
rewind: Option[RewindPoint]
|
2021-12-16 14:57:16 +00:00
|
|
|
|
blockVerifier: BlockVerifier
|
2022-03-03 08:05:33 +00:00
|
|
|
|
ident*: string
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
2022-03-03 08:05:33 +00:00
|
|
|
|
chronicles.formatIt SyncQueueKind: toLowerAscii($it)
|
|
|
|
|
|
|
|
|
|
template shortLog*[T](req: SyncRequest[T]): string =
|
|
|
|
|
Base10.toString(uint64(req.slot)) & ":" &
|
|
|
|
|
Base10.toString(req.count) & "@" &
|
|
|
|
|
Base10.toString(req.index)
|
|
|
|
|
|
|
|
|
|
chronicles.expandIt SyncRequest:
|
|
|
|
|
`it` = shortLog(it)
|
|
|
|
|
peer = shortLog(it.item)
|
|
|
|
|
direction = toLowerAscii($it.kind)
|
2022-01-20 07:25:45 +00:00
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
proc getShortMap*[T](req: SyncRequest[T],
|
2022-02-07 17:20:10 +00:00
|
|
|
|
data: openArray[ref ForkedSignedBeaconBlock]): string =
|
2021-12-08 21:15:29 +00:00
|
|
|
|
## Returns all slot numbers in ``data`` as placement map.
|
|
|
|
|
var res = newStringOfCap(req.count)
|
|
|
|
|
var slider = req.slot
|
|
|
|
|
var last = 0
|
|
|
|
|
for i in 0 ..< req.count:
|
|
|
|
|
if last < len(data):
|
|
|
|
|
for k in last ..< len(data):
|
2022-02-07 17:20:10 +00:00
|
|
|
|
if slider == data[k][].slot:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
res.add('x')
|
|
|
|
|
last = k + 1
|
|
|
|
|
break
|
2022-02-07 17:20:10 +00:00
|
|
|
|
elif slider < data[k][].slot:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
res.add('.')
|
|
|
|
|
break
|
|
|
|
|
else:
|
|
|
|
|
res.add('.')
|
2022-06-06 13:56:59 +00:00
|
|
|
|
slider = slider + 1
|
2021-12-08 21:15:29 +00:00
|
|
|
|
res
|
|
|
|
|
|
2023-02-11 20:48:35 +00:00
|
|
|
|
proc getShortMap*[T](req: SyncRequest[T],
|
2023-03-07 20:19:17 +00:00
|
|
|
|
data: openArray[ref BlobSidecar]): string =
|
2023-02-11 20:48:35 +00:00
|
|
|
|
## Returns all slot numbers in ``data`` as placement map.
|
2023-03-07 20:19:17 +00:00
|
|
|
|
var res = newStringOfCap(req.count * MAX_BLOBS_PER_BLOCK)
|
|
|
|
|
var cur : uint64 = 0
|
|
|
|
|
for slot in req.slot..<req.slot+req.count:
|
2023-05-06 08:58:50 +00:00
|
|
|
|
if cur >= lenu64(data):
|
|
|
|
|
res.add('|')
|
|
|
|
|
continue
|
2023-11-06 06:48:43 +00:00
|
|
|
|
if slot == data[cur].signed_block_header.message.slot:
|
2023-03-07 20:19:17 +00:00
|
|
|
|
for k in cur..<cur+MAX_BLOBS_PER_BLOCK:
|
2023-11-06 06:48:43 +00:00
|
|
|
|
if k >= lenu64(data) or slot != data[k].signed_block_header.message.slot:
|
2023-03-07 20:19:17 +00:00
|
|
|
|
res.add('|')
|
2023-02-11 20:48:35 +00:00
|
|
|
|
break
|
2023-05-06 08:58:50 +00:00
|
|
|
|
else:
|
|
|
|
|
inc(cur)
|
|
|
|
|
res.add('x')
|
2023-02-11 20:48:35 +00:00
|
|
|
|
else:
|
2023-03-07 20:19:17 +00:00
|
|
|
|
res.add('|')
|
2023-02-11 20:48:35 +00:00
|
|
|
|
res
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
proc contains*[T](req: SyncRequest[T], slot: Slot): bool {.inline.} =
|
2022-06-06 13:56:59 +00:00
|
|
|
|
slot >= req.slot and slot < req.slot + req.count
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc cmp*[T](a, b: SyncRequest[T]): int =
|
|
|
|
|
cmp(uint64(a.slot), uint64(b.slot))
|
|
|
|
|
|
|
|
|
|
proc checkResponse*[T](req: SyncRequest[T],
|
2023-02-11 20:48:35 +00:00
|
|
|
|
data: openArray[Slot]): bool =
|
2021-12-08 21:15:29 +00:00
|
|
|
|
if len(data) == 0:
|
|
|
|
|
# Impossible to verify empty response.
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
if uint64(len(data)) > req.count:
|
|
|
|
|
# Number of blocks in response should be less or equal to number of
|
|
|
|
|
# requested blocks.
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
var slot = req.slot
|
|
|
|
|
var rindex = 0'u64
|
|
|
|
|
var dindex = 0
|
|
|
|
|
|
|
|
|
|
while (rindex < req.count) and (dindex < len(data)):
|
2023-02-11 20:48:35 +00:00
|
|
|
|
if slot < data[dindex]:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
discard
|
2023-02-11 20:48:35 +00:00
|
|
|
|
elif slot == data[dindex]:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
inc(dindex)
|
|
|
|
|
else:
|
|
|
|
|
return false
|
2022-06-08 11:09:33 +00:00
|
|
|
|
slot += 1'u64
|
|
|
|
|
rindex += 1'u64
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
if dindex == len(data):
|
|
|
|
|
return true
|
|
|
|
|
else:
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
proc init[T](t1: typedesc[SyncRequest], kind: SyncQueueKind, start: Slot,
|
|
|
|
|
finish: Slot, t2: typedesc[T]): SyncRequest[T] =
|
|
|
|
|
let count = finish - start + 1'u64
|
2022-06-06 13:56:59 +00:00
|
|
|
|
SyncRequest[T](kind: kind, slot: start, count: count)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc init[T](t1: typedesc[SyncRequest], kind: SyncQueueKind, slot: Slot,
|
|
|
|
|
count: uint64, item: T): SyncRequest[T] =
|
2022-06-06 13:56:59 +00:00
|
|
|
|
SyncRequest[T](kind: kind, slot: slot, count: count, item: item)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc init[T](t1: typedesc[SyncRequest], kind: SyncQueueKind, start: Slot,
|
|
|
|
|
finish: Slot, item: T): SyncRequest[T] =
|
|
|
|
|
let count = finish - start + 1'u64
|
2022-06-06 13:56:59 +00:00
|
|
|
|
SyncRequest[T](kind: kind, slot: start, count: count, item: item)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc empty*[T](t: typedesc[SyncRequest], kind: SyncQueueKind,
|
|
|
|
|
t2: typedesc[T]): SyncRequest[T] {.inline.} =
|
2022-06-06 13:56:59 +00:00
|
|
|
|
SyncRequest[T](kind: kind, count: 0'u64)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc setItem*[T](sr: var SyncRequest[T], item: T) =
|
|
|
|
|
sr.item = item
|
|
|
|
|
|
|
|
|
|
proc isEmpty*[T](sr: SyncRequest[T]): bool {.inline.} =
|
2022-06-06 13:56:59 +00:00
|
|
|
|
(sr.count == 0'u64)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc init*[T](t1: typedesc[SyncQueue], t2: typedesc[T],
|
|
|
|
|
queueKind: SyncQueueKind,
|
|
|
|
|
start, final: Slot, chunkSize: uint64,
|
|
|
|
|
getSafeSlotCb: GetSlotCallback,
|
2021-12-16 14:57:16 +00:00
|
|
|
|
blockVerifier: BlockVerifier,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
syncQueueSize: int = -1,
|
|
|
|
|
ident: string = "main"): SyncQueue[T] =
|
2021-12-08 21:15:29 +00:00
|
|
|
|
## Create new synchronization queue with parameters
|
|
|
|
|
##
|
2022-11-11 14:36:02 +00:00
|
|
|
|
## ``start`` and ``final`` are starting and final Slots.
|
2021-12-08 21:15:29 +00:00
|
|
|
|
##
|
|
|
|
|
## ``chunkSize`` maximum number of slots in one request.
|
|
|
|
|
##
|
|
|
|
|
## ``syncQueueSize`` maximum queue size for incoming data.
|
|
|
|
|
## If ``syncQueueSize > 0`` queue will help to keep backpressure under
|
|
|
|
|
## control. If ``syncQueueSize <= 0`` then queue size is unlimited (default).
|
|
|
|
|
|
|
|
|
|
# SyncQueue is the core of sync manager, this data structure distributes
|
|
|
|
|
# requests to peers and manages responses from peers.
|
|
|
|
|
#
|
|
|
|
|
# Because SyncQueue is async data structure it manages backpressure and
|
|
|
|
|
# order of incoming responses and it also resolves "joker's" problem.
|
|
|
|
|
#
|
|
|
|
|
# Joker's problem
|
|
|
|
|
#
|
2022-11-11 14:36:02 +00:00
|
|
|
|
# According to pre-v0.12.0 Ethereum consensus network specification
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# > Clients MUST respond with at least one block, if they have it and it
|
|
|
|
|
# > exists in the range. Clients MAY limit the number of blocks in the
|
|
|
|
|
# > response.
|
2022-11-11 14:36:02 +00:00
|
|
|
|
# https://github.com/ethereum/consensus-specs/blob/v0.11.3/specs/phase0/p2p-interface.md#L590
|
2021-12-08 21:15:29 +00:00
|
|
|
|
#
|
|
|
|
|
# Such rule can lead to very uncertain responses, for example let slots from
|
|
|
|
|
# 10 to 12 will be not empty. Client which follows specification can answer
|
|
|
|
|
# with any response from this list (X - block, `-` empty space):
|
|
|
|
|
#
|
|
|
|
|
# 1. X X X
|
|
|
|
|
# 2. - - X
|
|
|
|
|
# 3. - X -
|
|
|
|
|
# 4. - X X
|
|
|
|
|
# 5. X - -
|
|
|
|
|
# 6. X - X
|
|
|
|
|
# 7. X X -
|
|
|
|
|
#
|
2022-11-11 14:36:02 +00:00
|
|
|
|
# If peer answers with `1` everything will be fine and `block_processor`
|
|
|
|
|
# will be able to process all 3 blocks.
|
|
|
|
|
# In case of `2`, `3`, `4`, `6` - `block_processor` will fail immediately
|
|
|
|
|
# with chunk and report "parent is missing" error.
|
|
|
|
|
# But in case of `5` and `7` blocks will be processed by `block_processor`
|
|
|
|
|
# without any problems, however it will start producing problems right from
|
|
|
|
|
# this uncertain last slot. SyncQueue will start producing requests for next
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# blocks, but all the responses from this point will fail with "parent is
|
|
|
|
|
# missing" error. Lets call such peers "jokers", because they are joking
|
|
|
|
|
# with responses.
|
|
|
|
|
#
|
|
|
|
|
# To fix "joker" problem we going to perform rollback to the latest finalized
|
|
|
|
|
# epoch's first slot.
|
2022-11-11 14:36:02 +00:00
|
|
|
|
#
|
|
|
|
|
# Note that as of spec v0.12.0, well-behaving clients are forbidden from
|
|
|
|
|
# answering this way. However, it still makes sense to attempt to handle
|
|
|
|
|
# this case to increase compatibility (e.g., with weak subjectivity nodes
|
|
|
|
|
# that are still backfilling blocks)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
doAssert(chunkSize > 0'u64, "Chunk size should not be zero")
|
|
|
|
|
SyncQueue[T](
|
|
|
|
|
kind: queueKind,
|
|
|
|
|
startSlot: start,
|
|
|
|
|
finalSlot: final,
|
|
|
|
|
chunkSize: chunkSize,
|
|
|
|
|
queueSize: syncQueueSize,
|
|
|
|
|
getSafeSlot: getSafeSlotCb,
|
2021-12-16 14:57:16 +00:00
|
|
|
|
waiters: newSeq[SyncWaiter](),
|
2021-12-08 21:15:29 +00:00
|
|
|
|
counter: 1'u64,
|
|
|
|
|
pending: initTable[uint64, SyncRequest[T]](),
|
|
|
|
|
debtsQueue: initHeapQueue[SyncRequest[T]](),
|
|
|
|
|
inpSlot: start,
|
|
|
|
|
outSlot: start,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
blockVerifier: blockVerifier,
|
|
|
|
|
ident: ident
|
2021-12-08 21:15:29 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
proc `<`*[T](a, b: SyncRequest[T]): bool =
|
|
|
|
|
doAssert(a.kind == b.kind)
|
|
|
|
|
case a.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
a.slot < b.slot
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
a.slot > b.slot
|
|
|
|
|
|
|
|
|
|
proc `<`*[T](a, b: SyncResult[T]): bool =
|
|
|
|
|
doAssert(a.request.kind == b.request.kind)
|
|
|
|
|
case a.request.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
a.request.slot < b.request.slot
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
a.request.slot > b.request.slot
|
|
|
|
|
|
|
|
|
|
proc `==`*[T](a, b: SyncRequest[T]): bool =
|
2022-06-06 13:56:59 +00:00
|
|
|
|
(a.kind == b.kind) and (a.slot == b.slot) and (a.count == b.count)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc lastSlot*[T](req: SyncRequest[T]): Slot =
|
|
|
|
|
## Returns last slot for request ``req``.
|
|
|
|
|
req.slot + req.count - 1'u64
|
|
|
|
|
|
|
|
|
|
proc makePending*[T](sq: SyncQueue[T], req: var SyncRequest[T]) =
|
|
|
|
|
req.index = sq.counter
|
|
|
|
|
sq.counter = sq.counter + 1'u64
|
|
|
|
|
sq.pending[req.index] = req
|
|
|
|
|
|
|
|
|
|
proc updateLastSlot*[T](sq: SyncQueue[T], last: Slot) {.inline.} =
|
|
|
|
|
## Update last slot stored in queue ``sq`` with value ``last``.
|
2022-12-23 07:42:55 +00:00
|
|
|
|
sq.finalSlot = last
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
2021-12-16 14:57:16 +00:00
|
|
|
|
proc wakeupWaiters[T](sq: SyncQueue[T], reset = false) =
|
2021-12-08 21:15:29 +00:00
|
|
|
|
## Wakeup one or all blocked waiters.
|
|
|
|
|
for item in sq.waiters:
|
2021-12-16 14:57:16 +00:00
|
|
|
|
if reset:
|
|
|
|
|
item.reset = true
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
if not(item.future.finished()):
|
2021-12-16 14:57:16 +00:00
|
|
|
|
item.future.complete()
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
2021-12-16 14:57:16 +00:00
|
|
|
|
proc waitForChanges[T](sq: SyncQueue[T]): Future[bool] {.async.} =
|
2021-12-08 21:15:29 +00:00
|
|
|
|
## Create new waiter and wait for completion from `wakeupWaiters()`.
|
2021-12-16 14:57:16 +00:00
|
|
|
|
var waitfut = newFuture[void]("SyncQueue.waitForChanges")
|
|
|
|
|
let waititem = SyncWaiter(future: waitfut)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
sq.waiters.add(waititem)
|
|
|
|
|
try:
|
2021-12-16 14:57:16 +00:00
|
|
|
|
await waitfut
|
|
|
|
|
return waititem.reset
|
2021-12-08 21:15:29 +00:00
|
|
|
|
finally:
|
|
|
|
|
sq.waiters.delete(sq.waiters.find(waititem))
|
|
|
|
|
|
|
|
|
|
proc wakeupAndWaitWaiters[T](sq: SyncQueue[T]) {.async.} =
|
2022-11-11 14:36:02 +00:00
|
|
|
|
## This procedure will perform wakeupWaiters(true) and blocks until last
|
2021-12-08 21:15:29 +00:00
|
|
|
|
## waiter will be awakened.
|
2021-12-16 14:57:16 +00:00
|
|
|
|
var waitChanges = sq.waitForChanges()
|
|
|
|
|
sq.wakeupWaiters(true)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
discard await waitChanges
|
|
|
|
|
|
2022-01-20 07:25:45 +00:00
|
|
|
|
proc clearAndWakeup*[T](sq: SyncQueue[T]) =
|
|
|
|
|
sq.pending.clear()
|
|
|
|
|
sq.wakeupWaiters(true)
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
proc resetWait*[T](sq: SyncQueue[T], toSlot: Option[Slot]) {.async.} =
|
|
|
|
|
## Perform reset of all the blocked waiters in SyncQueue.
|
|
|
|
|
##
|
|
|
|
|
## We adding one more waiter to the waiters sequence and
|
2021-12-16 14:57:16 +00:00
|
|
|
|
## call wakeupWaiters(true). Because our waiter is last in sequence of
|
2021-12-08 21:15:29 +00:00
|
|
|
|
## waiters it will be resumed only after all waiters will be awakened and
|
|
|
|
|
## finished.
|
|
|
|
|
|
|
|
|
|
# We are clearing pending list, so that all requests that are still running
|
|
|
|
|
# around (still downloading, but not yet pushed to the SyncQueue) will be
|
|
|
|
|
# expired. Its important to perform this call first (before await), otherwise
|
|
|
|
|
# you can introduce race problem.
|
|
|
|
|
sq.pending.clear()
|
|
|
|
|
|
|
|
|
|
# We calculating minimal slot number to which we will be able to reset,
|
|
|
|
|
# without missing any blocks. There 3 sources:
|
|
|
|
|
# 1. Debts queue.
|
|
|
|
|
# 2. Processing queue (`inpSlot`, `outSlot`).
|
|
|
|
|
# 3. Requested slot `toSlot`.
|
|
|
|
|
#
|
|
|
|
|
# Queue's `outSlot` is the lowest slot we added to `block_pool`, but
|
|
|
|
|
# `toSlot` slot can be less then `outSlot`. `debtsQueue` holds only not
|
|
|
|
|
# added slot requests, so it can't be bigger then `outSlot` value.
|
|
|
|
|
let minSlot =
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
if toSlot.isSome():
|
|
|
|
|
min(toSlot.get(), sq.outSlot)
|
|
|
|
|
else:
|
|
|
|
|
sq.outSlot
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
if toSlot.isSome():
|
|
|
|
|
toSlot.get()
|
|
|
|
|
else:
|
|
|
|
|
sq.outSlot
|
|
|
|
|
sq.debtsQueue.clear()
|
|
|
|
|
sq.debtsCount = 0
|
|
|
|
|
sq.readyQueue.clear()
|
|
|
|
|
sq.inpSlot = minSlot
|
|
|
|
|
sq.outSlot = minSlot
|
|
|
|
|
# We are going to wakeup all the waiters and wait for last one.
|
|
|
|
|
await sq.wakeupAndWaitWaiters()
|
|
|
|
|
|
|
|
|
|
proc isEmpty*[T](sr: SyncResult[T]): bool {.inline.} =
|
|
|
|
|
## Returns ``true`` if response chain of blocks is empty (has only empty
|
|
|
|
|
## slots).
|
|
|
|
|
len(sr.data) == 0
|
|
|
|
|
|
|
|
|
|
proc hasEndGap*[T](sr: SyncResult[T]): bool {.inline.} =
|
|
|
|
|
## Returns ``true`` if response chain of blocks has gap at the end.
|
|
|
|
|
let lastslot = sr.request.slot + sr.request.count - 1'u64
|
|
|
|
|
if len(sr.data) == 0:
|
|
|
|
|
return true
|
2022-02-07 17:20:10 +00:00
|
|
|
|
if sr.data[^1][].slot != lastslot:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
proc getLastNonEmptySlot*[T](sr: SyncResult[T]): Slot {.inline.} =
|
|
|
|
|
## Returns last non-empty slot from result ``sr``. If response has only
|
|
|
|
|
## empty slots, original request slot will be returned.
|
|
|
|
|
if len(sr.data) == 0:
|
|
|
|
|
# If response has only empty slots we going to use original request slot
|
|
|
|
|
sr.request.slot
|
|
|
|
|
else:
|
2022-02-07 17:20:10 +00:00
|
|
|
|
sr.data[^1][].slot
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
2022-09-19 09:37:42 +00:00
|
|
|
|
proc processGap[T](sq: SyncQueue[T], sr: SyncResult[T]) =
|
|
|
|
|
if sr.isEmpty():
|
|
|
|
|
let gitem = GapItem[T](start: sr.request.slot,
|
|
|
|
|
finish: sr.request.slot + sr.request.count - 1'u64,
|
|
|
|
|
item: sr.request.item)
|
|
|
|
|
sq.gapList.add(gitem)
|
|
|
|
|
else:
|
|
|
|
|
if sr.hasEndGap():
|
|
|
|
|
let gitem = GapItem[T](start: sr.getLastNonEmptySlot() + 1'u64,
|
|
|
|
|
finish: sr.request.slot + sr.request.count - 1'u64,
|
|
|
|
|
item: sr.request.item)
|
|
|
|
|
sq.gapList.add(gitem)
|
|
|
|
|
else:
|
|
|
|
|
sq.gapList.reset()
|
|
|
|
|
|
|
|
|
|
proc rewardForGaps[T](sq: SyncQueue[T], score: int) =
|
|
|
|
|
mixin updateScore, getStats
|
|
|
|
|
logScope:
|
|
|
|
|
sync_ident = sq.ident
|
|
|
|
|
direction = sq.kind
|
|
|
|
|
topics = "syncman"
|
|
|
|
|
|
|
|
|
|
for gap in sq.gapList:
|
|
|
|
|
if score < 0:
|
|
|
|
|
# Every empty response increases penalty by 25%, but not more than 200%.
|
|
|
|
|
let
|
|
|
|
|
emptyCount = gap.item.getStats(SyncResponseKind.Empty)
|
|
|
|
|
goodCount = gap.item.getStats(SyncResponseKind.Good)
|
|
|
|
|
|
|
|
|
|
if emptyCount <= goodCount:
|
|
|
|
|
gap.item.updateScore(score)
|
|
|
|
|
else:
|
|
|
|
|
let
|
|
|
|
|
weight = int(min(emptyCount - goodCount, 8'u64))
|
|
|
|
|
newScore = score + score * weight div 4
|
|
|
|
|
gap.item.updateScore(newScore)
|
|
|
|
|
debug "Peer received gap penalty", peer = gap.item,
|
|
|
|
|
penalty = newScore
|
|
|
|
|
else:
|
|
|
|
|
gap.item.updateScore(score)
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
proc toDebtsQueue[T](sq: SyncQueue[T], sr: SyncRequest[T]) =
|
|
|
|
|
sq.debtsQueue.push(sr)
|
|
|
|
|
sq.debtsCount = sq.debtsCount + sr.count
|
|
|
|
|
|
|
|
|
|
proc getRewindPoint*[T](sq: SyncQueue[T], failSlot: Slot,
|
|
|
|
|
safeSlot: Slot): Slot =
|
2022-03-03 08:05:33 +00:00
|
|
|
|
logScope:
|
|
|
|
|
sync_ident = sq.ident
|
|
|
|
|
direction = sq.kind
|
|
|
|
|
topics = "syncman"
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
# Calculate the latest finalized epoch.
|
2022-01-11 10:01:54 +00:00
|
|
|
|
let finalizedEpoch = epoch(safeSlot)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
# Calculate failure epoch.
|
2022-01-11 10:01:54 +00:00
|
|
|
|
let failEpoch = epoch(failSlot)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
# Calculate exponential rewind point in number of epochs.
|
|
|
|
|
let epochCount =
|
|
|
|
|
if sq.rewind.isSome():
|
|
|
|
|
let rewind = sq.rewind.get()
|
|
|
|
|
if failSlot == rewind.failSlot:
|
|
|
|
|
# `MissingParent` happened at same slot so we increase rewind point by
|
|
|
|
|
# factor of 2.
|
|
|
|
|
if failEpoch > finalizedEpoch:
|
|
|
|
|
let rewindPoint = rewind.epochCount shl 1
|
|
|
|
|
if rewindPoint < rewind.epochCount:
|
|
|
|
|
# If exponential rewind point produces `uint64` overflow we will
|
|
|
|
|
# make rewind to latest finalized epoch.
|
|
|
|
|
failEpoch - finalizedEpoch
|
|
|
|
|
else:
|
|
|
|
|
if (failEpoch < rewindPoint) or
|
|
|
|
|
(failEpoch - rewindPoint < finalizedEpoch):
|
|
|
|
|
# If exponential rewind point points to position which is far
|
|
|
|
|
# behind latest finalized epoch.
|
|
|
|
|
failEpoch - finalizedEpoch
|
|
|
|
|
else:
|
|
|
|
|
rewindPoint
|
|
|
|
|
else:
|
|
|
|
|
warn "Trying to rewind over the last finalized epoch",
|
|
|
|
|
finalized_slot = safeSlot, fail_slot = failSlot,
|
|
|
|
|
finalized_epoch = finalizedEpoch, fail_epoch = failEpoch,
|
|
|
|
|
rewind_epoch_count = rewind.epochCount,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
finalized_epoch = finalizedEpoch
|
2021-12-08 21:15:29 +00:00
|
|
|
|
0'u64
|
|
|
|
|
else:
|
|
|
|
|
# `MissingParent` happened at different slot so we going to rewind for
|
|
|
|
|
# 1 epoch only.
|
|
|
|
|
if (failEpoch < 1'u64) or (failEpoch - 1'u64 < finalizedEpoch):
|
|
|
|
|
warn "Сould not rewind further than the last finalized epoch",
|
|
|
|
|
finalized_slot = safeSlot, fail_slot = failSlot,
|
|
|
|
|
finalized_epoch = finalizedEpoch, fail_epoch = failEpoch,
|
|
|
|
|
rewind_epoch_count = rewind.epochCount,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
finalized_epoch = finalizedEpoch
|
2021-12-08 21:15:29 +00:00
|
|
|
|
0'u64
|
|
|
|
|
else:
|
|
|
|
|
1'u64
|
|
|
|
|
else:
|
|
|
|
|
# `MissingParent` happened first time.
|
|
|
|
|
if (failEpoch < 1'u64) or (failEpoch - 1'u64 < finalizedEpoch):
|
|
|
|
|
warn "Сould not rewind further than the last finalized epoch",
|
|
|
|
|
finalized_slot = safeSlot, fail_slot = failSlot,
|
|
|
|
|
finalized_epoch = finalizedEpoch, fail_epoch = failEpoch,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
finalized_epoch = finalizedEpoch
|
2021-12-08 21:15:29 +00:00
|
|
|
|
0'u64
|
|
|
|
|
else:
|
|
|
|
|
1'u64
|
|
|
|
|
|
|
|
|
|
if epochCount == 0'u64:
|
|
|
|
|
warn "Unable to continue syncing, please restart the node",
|
|
|
|
|
finalized_slot = safeSlot, fail_slot = failSlot,
|
|
|
|
|
finalized_epoch = finalizedEpoch, fail_epoch = failEpoch,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
finalized_epoch = finalizedEpoch
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# Calculate the rewind epoch, which will be equal to last rewind point or
|
|
|
|
|
# finalizedEpoch
|
|
|
|
|
let rewindEpoch =
|
|
|
|
|
if sq.rewind.isNone():
|
|
|
|
|
finalizedEpoch
|
|
|
|
|
else:
|
2022-01-11 10:01:54 +00:00
|
|
|
|
epoch(sq.rewind.get().failSlot) - sq.rewind.get().epochCount
|
|
|
|
|
rewindEpoch.start_slot()
|
2021-12-08 21:15:29 +00:00
|
|
|
|
else:
|
|
|
|
|
# Calculate the rewind epoch, which should not be less than the latest
|
|
|
|
|
# finalized epoch.
|
|
|
|
|
let rewindEpoch = failEpoch - epochCount
|
|
|
|
|
# Update and save new rewind point in SyncQueue.
|
|
|
|
|
sq.rewind = some(RewindPoint(failSlot: failSlot, epochCount: epochCount))
|
2022-01-11 10:01:54 +00:00
|
|
|
|
rewindEpoch.start_slot()
|
2021-12-08 21:15:29 +00:00
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
# While we perform backward sync, the only possible slot we could rewind is
|
|
|
|
|
# latest stored block.
|
|
|
|
|
if failSlot == safeSlot:
|
|
|
|
|
warn "Unable to continue syncing, please restart the node",
|
2022-03-03 08:05:33 +00:00
|
|
|
|
safe_slot = safeSlot, fail_slot = failSlot
|
2021-12-08 21:15:29 +00:00
|
|
|
|
safeSlot
|
|
|
|
|
|
2023-06-05 14:42:27 +00:00
|
|
|
|
# This belongs inside the blocks iterator below, but can't be there due to
|
|
|
|
|
# https://github.com/nim-lang/Nim/issues/21242
|
|
|
|
|
func getOpt(blobs: Opt[seq[BlobSidecars]], i: int): Opt[BlobSidecars] =
|
|
|
|
|
if blobs.isSome:
|
|
|
|
|
Opt.some(blobs.get()[i])
|
|
|
|
|
else:
|
|
|
|
|
Opt.none(BlobSidecars)
|
|
|
|
|
|
|
|
|
|
iterator blocks[T](sq: SyncQueue[T],
|
|
|
|
|
sr: SyncResult[T]): (ref ForkedSignedBeaconBlock, Opt[BlobSidecars]) =
|
2021-12-08 21:15:29 +00:00
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
for i in countup(0, len(sr.data) - 1):
|
2023-06-05 14:42:27 +00:00
|
|
|
|
yield (sr.data[i], sr.blobs.getOpt(i))
|
2021-12-08 21:15:29 +00:00
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
for i in countdown(len(sr.data) - 1, 0):
|
2023-06-05 14:42:27 +00:00
|
|
|
|
yield (sr.data[i], sr.blobs.getOpt(i))
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc advanceOutput*[T](sq: SyncQueue[T], number: uint64) =
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
sq.outSlot = sq.outSlot + number
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
sq.outSlot = sq.outSlot - number
|
|
|
|
|
|
|
|
|
|
proc advanceInput[T](sq: SyncQueue[T], number: uint64) =
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
sq.inpSlot = sq.inpSlot + number
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
sq.inpSlot = sq.inpSlot - number
|
|
|
|
|
|
2021-12-16 14:57:16 +00:00
|
|
|
|
proc notInRange[T](sq: SyncQueue[T], sr: SyncRequest[T]): bool =
|
2021-12-08 21:15:29 +00:00
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
2022-03-15 17:56:56 +00:00
|
|
|
|
(sq.queueSize > 0) and (sr.slot > sq.outSlot)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
of SyncQueueKind.Backward:
|
2022-03-15 17:56:56 +00:00
|
|
|
|
(sq.queueSize > 0) and (sr.lastSlot < sq.outSlot)
|
|
|
|
|
|
|
|
|
|
func numAlreadyKnownSlots[T](sq: SyncQueue[T], sr: SyncRequest[T]): uint64 =
|
|
|
|
|
## Compute the number of slots covered by a given `SyncRequest` that are
|
|
|
|
|
## already known and, hence, no longer relevant for sync progression.
|
|
|
|
|
let
|
|
|
|
|
outSlot = sq.outSlot
|
|
|
|
|
lowSlot = sr.slot
|
|
|
|
|
highSlot = sr.lastSlot
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
if outSlot > highSlot:
|
|
|
|
|
# Entire request is no longer relevant.
|
|
|
|
|
sr.count
|
|
|
|
|
elif outSlot > lowSlot:
|
|
|
|
|
# Request is only partially relevant.
|
|
|
|
|
outSlot - lowSlot
|
|
|
|
|
else:
|
|
|
|
|
# Entire request is still relevant.
|
|
|
|
|
0
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
if lowSlot > outSlot:
|
|
|
|
|
# Entire request is no longer relevant.
|
|
|
|
|
sr.count
|
|
|
|
|
elif highSlot > outSlot:
|
|
|
|
|
# Request is only partially relevant.
|
|
|
|
|
highSlot - outSlot
|
|
|
|
|
else:
|
|
|
|
|
# Entire request is still relevant.
|
|
|
|
|
0
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T],
|
2022-02-07 17:20:10 +00:00
|
|
|
|
data: seq[ref ForkedSignedBeaconBlock],
|
2023-03-07 20:19:17 +00:00
|
|
|
|
blobs: Opt[seq[BlobSidecars]],
|
2023-02-06 07:22:08 +00:00
|
|
|
|
maybeFinalized: bool = false,
|
2021-12-16 14:57:16 +00:00
|
|
|
|
processingCb: ProcessingCallback = nil) {.async.} =
|
2022-03-03 08:05:33 +00:00
|
|
|
|
logScope:
|
|
|
|
|
sync_ident = sq.ident
|
|
|
|
|
topics = "syncman"
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
## Push successful result to queue ``sq``.
|
2022-09-19 09:37:42 +00:00
|
|
|
|
mixin updateScore, updateStats, getStats
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
if sr.index notin sq.pending:
|
|
|
|
|
# If request `sr` not in our pending list, it only means that
|
|
|
|
|
# SyncQueue.resetWait() happens and all pending requests are expired, so
|
|
|
|
|
# we swallow `old` requests, and in such way sync-workers are able to get
|
|
|
|
|
# proper new requests from SyncQueue.
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
sq.pending.del(sr.index)
|
|
|
|
|
|
|
|
|
|
# This is backpressure handling algorithm, this algorithm is blocking
|
2021-12-16 14:57:16 +00:00
|
|
|
|
# all pending `push` requests if `request.slot` not in range.
|
2021-12-08 21:15:29 +00:00
|
|
|
|
while true:
|
2021-12-16 14:57:16 +00:00
|
|
|
|
if sq.notInRange(sr):
|
|
|
|
|
let reset = await sq.waitForChanges()
|
|
|
|
|
if reset:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# SyncQueue reset happens. We are exiting to wake up sync-worker.
|
2021-12-16 14:57:16 +00:00
|
|
|
|
return
|
|
|
|
|
else:
|
2023-02-11 20:48:35 +00:00
|
|
|
|
let syncres = SyncResult[T](request: sr, data: data, blobs: blobs)
|
2021-12-16 14:57:16 +00:00
|
|
|
|
sq.readyQueue.push(syncres)
|
|
|
|
|
break
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
while len(sq.readyQueue) > 0:
|
|
|
|
|
let reqres =
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
let minSlot = sq.readyQueue[0].request.slot
|
2022-03-15 17:56:56 +00:00
|
|
|
|
if sq.outSlot < minSlot:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
none[SyncResult[T]]()
|
|
|
|
|
else:
|
|
|
|
|
some(sq.readyQueue.pop())
|
|
|
|
|
of SyncQueueKind.Backward:
|
2022-04-08 16:22:49 +00:00
|
|
|
|
let maxslot = sq.readyQueue[0].request.slot +
|
2021-12-08 21:15:29 +00:00
|
|
|
|
(sq.readyQueue[0].request.count - 1'u64)
|
2022-04-08 16:22:49 +00:00
|
|
|
|
if sq.outSlot > maxslot:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
none[SyncResult[T]]()
|
|
|
|
|
else:
|
|
|
|
|
some(sq.readyQueue.pop())
|
|
|
|
|
|
|
|
|
|
let item =
|
|
|
|
|
if reqres.isSome():
|
|
|
|
|
reqres.get()
|
|
|
|
|
else:
|
|
|
|
|
let rewindSlot = sq.getRewindPoint(sq.outSlot, sq.getSafeSlot())
|
|
|
|
|
warn "Got incorrect sync result in queue, rewind happens",
|
|
|
|
|
blocks_map = getShortMap(sq.readyQueue[0].request,
|
|
|
|
|
sq.readyQueue[0].data),
|
|
|
|
|
blocks_count = len(sq.readyQueue[0].data),
|
|
|
|
|
output_slot = sq.outSlot, input_slot = sq.inpSlot,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
rewind_to_slot = rewindSlot, request = sq.readyQueue[0].request
|
2021-12-08 21:15:29 +00:00
|
|
|
|
await sq.resetWait(some(rewindSlot))
|
|
|
|
|
break
|
|
|
|
|
|
2021-12-16 14:57:16 +00:00
|
|
|
|
if processingCb != nil:
|
|
|
|
|
processingCb()
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# Validating received blocks one by one
|
2022-01-26 12:20:08 +00:00
|
|
|
|
var
|
|
|
|
|
hasInvalidBlock = false
|
|
|
|
|
unviableBlock: Option[(Eth2Digest, Slot)]
|
|
|
|
|
missingParentSlot: Option[Slot]
|
2022-09-19 09:37:42 +00:00
|
|
|
|
goodBlock: Option[Slot]
|
2022-01-26 12:20:08 +00:00
|
|
|
|
|
2023-01-27 10:41:19 +00:00
|
|
|
|
# TODO when https://github.com/nim-lang/Nim/issues/21306 is fixed in used
|
|
|
|
|
# Nim versions, remove workaround and move `res` into for loop
|
2022-11-10 17:40:27 +00:00
|
|
|
|
res: Result[void, VerifierError]
|
2022-01-26 12:20:08 +00:00
|
|
|
|
|
2023-02-11 20:48:35 +00:00
|
|
|
|
var i=0
|
2023-06-05 14:42:27 +00:00
|
|
|
|
for blk, blb in sq.blocks(item):
|
|
|
|
|
res = await sq.blockVerifier(blk[], blb, maybeFinalized)
|
2023-02-11 20:48:35 +00:00
|
|
|
|
inc(i)
|
|
|
|
|
|
2022-01-26 12:20:08 +00:00
|
|
|
|
if res.isOk():
|
2022-09-19 09:37:42 +00:00
|
|
|
|
goodBlock = some(blk[].slot)
|
2022-01-26 12:20:08 +00:00
|
|
|
|
else:
|
|
|
|
|
case res.error()
|
2022-11-10 17:40:27 +00:00
|
|
|
|
of VerifierError.MissingParent:
|
2022-02-07 17:20:10 +00:00
|
|
|
|
missingParentSlot = some(blk[].slot)
|
2022-01-26 12:20:08 +00:00
|
|
|
|
break
|
2022-11-10 17:40:27 +00:00
|
|
|
|
of VerifierError.Duplicate:
|
2022-01-26 12:20:08 +00:00
|
|
|
|
# Keep going, happens naturally
|
|
|
|
|
discard
|
2022-11-10 17:40:27 +00:00
|
|
|
|
of VerifierError.UnviableFork:
|
2022-01-26 12:20:08 +00:00
|
|
|
|
# Keep going so as to register other unviable blocks with the
|
|
|
|
|
# quarantine
|
|
|
|
|
if unviableBlock.isNone:
|
|
|
|
|
# Remember the first unviable block, so we can log it
|
2022-02-07 17:20:10 +00:00
|
|
|
|
unviableBlock = some((blk[].root, blk[].slot))
|
2022-01-26 12:20:08 +00:00
|
|
|
|
|
2022-11-10 17:40:27 +00:00
|
|
|
|
of VerifierError.Invalid:
|
2022-01-26 12:20:08 +00:00
|
|
|
|
hasInvalidBlock = true
|
|
|
|
|
|
|
|
|
|
let req = item.request
|
2022-07-06 10:34:12 +00:00
|
|
|
|
notice "Received invalid sequence of blocks", request = req,
|
|
|
|
|
blocks_count = len(item.data),
|
|
|
|
|
blocks_map = getShortMap(req, item.data)
|
2022-11-11 11:34:28 +00:00
|
|
|
|
req.item.updateScore(PeerScoreBadValues)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
break
|
|
|
|
|
|
2022-01-26 12:20:08 +00:00
|
|
|
|
# When errors happen while processing blocks, we retry the same request
|
|
|
|
|
# with, hopefully, a different peer
|
|
|
|
|
let retryRequest =
|
|
|
|
|
hasInvalidBlock or unviableBlock.isSome() or missingParentSlot.isSome()
|
2022-09-19 09:37:42 +00:00
|
|
|
|
if not(retryRequest):
|
2022-03-15 17:56:56 +00:00
|
|
|
|
let numSlotsAdvanced = item.request.count - sq.numAlreadyKnownSlots(sr)
|
|
|
|
|
sq.advanceOutput(numSlotsAdvanced)
|
2022-01-26 12:20:08 +00:00
|
|
|
|
|
2022-09-19 09:37:42 +00:00
|
|
|
|
if goodBlock.isSome():
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# If there no error and response was not empty we should reward peer
|
2022-01-26 12:20:08 +00:00
|
|
|
|
# with some bonus score - not for duplicate blocks though.
|
2022-11-11 11:34:28 +00:00
|
|
|
|
item.request.item.updateScore(PeerScoreGoodValues)
|
2022-09-19 09:37:42 +00:00
|
|
|
|
item.request.item.updateStats(SyncResponseKind.Good, 1'u64)
|
|
|
|
|
|
|
|
|
|
# BlockProcessor reports good block, so we can reward all the peers
|
|
|
|
|
# who sent us empty responses.
|
2022-11-11 11:34:28 +00:00
|
|
|
|
sq.rewardForGaps(PeerScoreGoodValues)
|
2022-09-19 09:37:42 +00:00
|
|
|
|
sq.gapList.reset()
|
|
|
|
|
else:
|
|
|
|
|
# Response was empty
|
|
|
|
|
item.request.item.updateStats(SyncResponseKind.Empty, 1'u64)
|
|
|
|
|
|
|
|
|
|
sq.processGap(item)
|
2022-01-26 12:20:08 +00:00
|
|
|
|
|
2022-03-15 17:56:56 +00:00
|
|
|
|
if numSlotsAdvanced > 0:
|
|
|
|
|
sq.wakeupWaiters()
|
2021-12-08 21:15:29 +00:00
|
|
|
|
else:
|
2022-03-03 08:05:33 +00:00
|
|
|
|
debug "Block pool rejected peer's response", request = item.request,
|
2021-12-08 21:15:29 +00:00
|
|
|
|
blocks_map = getShortMap(item.request, item.data),
|
2022-01-26 12:20:08 +00:00
|
|
|
|
blocks_count = len(item.data),
|
2022-09-19 09:37:42 +00:00
|
|
|
|
ok = goodBlock.isSome(),
|
2022-01-26 12:20:08 +00:00
|
|
|
|
unviable = unviableBlock.isSome(),
|
2022-03-03 08:05:33 +00:00
|
|
|
|
missing_parent = missingParentSlot.isSome()
|
2022-01-26 12:20:08 +00:00
|
|
|
|
# We need to move failed response to the debts queue.
|
|
|
|
|
sq.toDebtsQueue(item.request)
|
|
|
|
|
|
2022-09-19 09:37:42 +00:00
|
|
|
|
if unviableBlock.isSome():
|
2022-01-26 12:20:08 +00:00
|
|
|
|
let req = item.request
|
2022-03-03 08:05:33 +00:00
|
|
|
|
notice "Received blocks from an unviable fork", request = req,
|
2022-01-26 12:20:08 +00:00
|
|
|
|
blockRoot = unviableBlock.get()[0],
|
2022-03-03 08:05:33 +00:00
|
|
|
|
blockSlot = unviableBlock.get()[1],
|
|
|
|
|
blocks_count = len(item.data),
|
|
|
|
|
blocks_map = getShortMap(req, item.data)
|
2022-01-26 12:20:08 +00:00
|
|
|
|
req.item.updateScore(PeerScoreUnviableFork)
|
|
|
|
|
|
2022-09-19 09:37:42 +00:00
|
|
|
|
if missingParentSlot.isSome():
|
2022-01-26 12:20:08 +00:00
|
|
|
|
var
|
|
|
|
|
resetSlot: Option[Slot]
|
|
|
|
|
failSlot = missingParentSlot.get()
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
2022-11-10 17:40:27 +00:00
|
|
|
|
# If we got `VerifierError.MissingParent` it means that peer returns
|
|
|
|
|
# chain of blocks with holes or `block_pool` is in incomplete state. We
|
|
|
|
|
# going to rewind the SyncQueue some distance back (2ⁿ, where n∈[0,∞],
|
|
|
|
|
# but no more than `finalized_epoch`).
|
2021-12-08 21:15:29 +00:00
|
|
|
|
let
|
|
|
|
|
req = item.request
|
|
|
|
|
safeSlot = sq.getSafeSlot()
|
2022-09-19 09:37:42 +00:00
|
|
|
|
gapsCount = len(sq.gapList)
|
|
|
|
|
|
|
|
|
|
# We should penalize all the peers which responded with gaps.
|
2022-11-11 11:34:28 +00:00
|
|
|
|
sq.rewardForGaps(PeerScoreMissingValues)
|
2022-09-19 09:37:42 +00:00
|
|
|
|
sq.gapList.reset()
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
2022-09-19 09:37:42 +00:00
|
|
|
|
if goodBlock.isSome():
|
2022-11-10 17:40:27 +00:00
|
|
|
|
# `VerifierError.MissingParent` and `Success` present in response,
|
2022-09-19 09:37:42 +00:00
|
|
|
|
# it means that we just need to request this range one more time.
|
|
|
|
|
debug "Unexpected missing parent, but no rewind needed",
|
|
|
|
|
request = req, finalized_slot = safeSlot,
|
|
|
|
|
last_good_slot = goodBlock.get(),
|
|
|
|
|
missing_parent_slot = missingParentSlot.get(),
|
|
|
|
|
blocks_count = len(item.data),
|
|
|
|
|
blocks_map = getShortMap(req, item.data),
|
|
|
|
|
gaps_count = gapsCount
|
2022-11-11 11:34:28 +00:00
|
|
|
|
req.item.updateScore(PeerScoreMissingValues)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
else:
|
2022-09-19 09:37:42 +00:00
|
|
|
|
if safeSlot < req.slot:
|
|
|
|
|
let rewindSlot = sq.getRewindPoint(failSlot, safeSlot)
|
|
|
|
|
debug "Unexpected missing parent, rewind happens",
|
|
|
|
|
request = req, rewind_to_slot = rewindSlot,
|
|
|
|
|
rewind_point = sq.rewind, finalized_slot = safeSlot,
|
|
|
|
|
blocks_count = len(item.data),
|
|
|
|
|
blocks_map = getShortMap(req, item.data),
|
|
|
|
|
gaps_count = gapsCount
|
|
|
|
|
resetSlot = some(rewindSlot)
|
|
|
|
|
else:
|
|
|
|
|
error "Unexpected missing parent at finalized epoch slot",
|
2022-03-03 08:05:33 +00:00
|
|
|
|
request = req, rewind_to_slot = safeSlot,
|
|
|
|
|
blocks_count = len(item.data),
|
2022-09-19 09:37:42 +00:00
|
|
|
|
blocks_map = getShortMap(req, item.data),
|
|
|
|
|
gaps_count = gapsCount
|
2022-11-11 11:34:28 +00:00
|
|
|
|
req.item.updateScore(PeerScoreBadValues)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
of SyncQueueKind.Backward:
|
2022-09-16 19:45:53 +00:00
|
|
|
|
if safeSlot > failSlot:
|
2022-01-26 12:20:08 +00:00
|
|
|
|
let rewindSlot = sq.getRewindPoint(failSlot, safeSlot)
|
2022-01-20 07:25:45 +00:00
|
|
|
|
# It's quite common peers give us fewer blocks than we ask for
|
2022-12-23 07:42:55 +00:00
|
|
|
|
debug "Gap in block range response, rewinding", request = req,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
rewind_to_slot = rewindSlot, rewind_fail_slot = failSlot,
|
|
|
|
|
finalized_slot = safeSlot, blocks_count = len(item.data),
|
|
|
|
|
blocks_map = getShortMap(req, item.data)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
resetSlot = some(rewindSlot)
|
2022-11-11 11:34:28 +00:00
|
|
|
|
req.item.updateScore(PeerScoreMissingValues)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
else:
|
2022-03-03 08:05:33 +00:00
|
|
|
|
error "Unexpected missing parent at safe slot", request = req,
|
|
|
|
|
to_slot = safeSlot, blocks_count = len(item.data),
|
|
|
|
|
blocks_map = getShortMap(req, item.data)
|
2022-11-11 11:34:28 +00:00
|
|
|
|
req.item.updateScore(PeerScoreBadValues)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
2022-01-26 12:20:08 +00:00
|
|
|
|
if resetSlot.isSome():
|
|
|
|
|
await sq.resetWait(resetSlot)
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
2022-03-03 08:05:33 +00:00
|
|
|
|
debug "Rewind to slot has happened", reset_slot = resetSlot.get(),
|
2022-01-26 12:20:08 +00:00
|
|
|
|
queue_input_slot = sq.inpSlot, queue_output_slot = sq.outSlot,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
rewind_point = sq.rewind, direction = sq.kind
|
2022-01-26 12:20:08 +00:00
|
|
|
|
of SyncQueueKind.Backward:
|
2022-03-03 08:05:33 +00:00
|
|
|
|
debug "Rewind to slot has happened", reset_slot = resetSlot.get(),
|
2022-01-26 12:20:08 +00:00
|
|
|
|
queue_input_slot = sq.inpSlot, queue_output_slot = sq.outSlot,
|
2022-03-03 08:05:33 +00:00
|
|
|
|
direction = sq.kind
|
2022-01-26 12:20:08 +00:00
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T]) =
|
|
|
|
|
## Push failed request back to queue.
|
|
|
|
|
if sr.index notin sq.pending:
|
|
|
|
|
# If request `sr` not in our pending list, it only means that
|
|
|
|
|
# SyncQueue.resetWait() happens and all pending requests are expired, so
|
|
|
|
|
# we swallow `old` requests, and in such way sync-workers are able to get
|
|
|
|
|
# proper new requests from SyncQueue.
|
|
|
|
|
return
|
|
|
|
|
sq.pending.del(sr.index)
|
|
|
|
|
sq.toDebtsQueue(sr)
|
|
|
|
|
|
2022-03-15 17:56:56 +00:00
|
|
|
|
proc handlePotentialSafeSlotAdvancement[T](sq: SyncQueue[T]) =
|
|
|
|
|
# It may happen that sync progress advanced to a newer `safeSlot`, either
|
|
|
|
|
# by a response that started with good values and only had errors late, or
|
|
|
|
|
# through an out-of-band mechanism, e.g., VC / REST.
|
|
|
|
|
# If that happens, advance to the new `safeSlot` to avoid repeating requests
|
|
|
|
|
# for data that is considered immutable and no longer relevant.
|
2022-05-10 11:46:14 +00:00
|
|
|
|
let safeSlot = sq.getSafeSlot()
|
|
|
|
|
func numSlotsBehindSafeSlot(slot: Slot): uint64 =
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
if safeSlot > slot:
|
|
|
|
|
safeSlot - slot
|
|
|
|
|
else:
|
|
|
|
|
0
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
if slot > safeSlot:
|
|
|
|
|
slot - safeSlot
|
|
|
|
|
else:
|
|
|
|
|
0
|
|
|
|
|
|
2022-03-15 17:56:56 +00:00
|
|
|
|
let
|
2022-05-10 11:46:14 +00:00
|
|
|
|
numOutSlotsAdvanced = sq.outSlot.numSlotsBehindSafeSlot
|
|
|
|
|
numInpSlotsAdvanced =
|
2022-03-15 17:56:56 +00:00
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
2022-05-10 11:46:14 +00:00
|
|
|
|
sq.inpSlot.numSlotsBehindSafeSlot
|
2022-03-15 17:56:56 +00:00
|
|
|
|
of SyncQueueKind.Backward:
|
2022-05-10 11:46:14 +00:00
|
|
|
|
if sq.inpSlot == 0xFFFF_FFFF_FFFF_FFFF'u64:
|
|
|
|
|
0'u64
|
2022-03-15 17:56:56 +00:00
|
|
|
|
else:
|
2022-05-10 11:46:14 +00:00
|
|
|
|
sq.inpSlot.numSlotsBehindSafeSlot
|
|
|
|
|
if numOutSlotsAdvanced != 0 or numInpSlotsAdvanced != 0:
|
2022-03-15 17:56:56 +00:00
|
|
|
|
debug "Sync progress advanced out-of-band",
|
2022-05-10 11:46:14 +00:00
|
|
|
|
safeSlot, outSlot = sq.outSlot, inpSlot = sq.inpSlot
|
|
|
|
|
if numOutSlotsAdvanced != 0:
|
|
|
|
|
sq.advanceOutput(numOutSlotsAdvanced)
|
|
|
|
|
if numInpSlotsAdvanced != 0:
|
|
|
|
|
sq.advanceInput(numInpSlotsAdvanced)
|
2022-03-15 17:56:56 +00:00
|
|
|
|
sq.wakeupWaiters()
|
|
|
|
|
|
|
|
|
|
func updateRequestForNewSafeSlot[T](sq: SyncQueue[T], sr: var SyncRequest[T]) =
|
|
|
|
|
# Requests may have originated before the latest `safeSlot` advancement.
|
|
|
|
|
# Update it to not request any data prior to `safeSlot`.
|
|
|
|
|
let
|
|
|
|
|
outSlot = sq.outSlot
|
|
|
|
|
lowSlot = sr.slot
|
|
|
|
|
highSlot = sr.lastSlot
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
if outSlot <= lowSlot:
|
|
|
|
|
# Entire request is still relevant.
|
|
|
|
|
discard
|
|
|
|
|
elif outSlot <= highSlot:
|
|
|
|
|
# Request is only partially relevant.
|
|
|
|
|
let
|
|
|
|
|
numSlotsDone = outSlot - lowSlot
|
2022-06-06 13:56:59 +00:00
|
|
|
|
sr.slot += numSlotsDone
|
|
|
|
|
sr.count -= numSlotsDone
|
2022-03-15 17:56:56 +00:00
|
|
|
|
else:
|
|
|
|
|
# Entire request is no longer relevant.
|
|
|
|
|
sr.count = 0
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
if outSlot >= highSlot:
|
|
|
|
|
# Entire request is still relevant.
|
|
|
|
|
discard
|
|
|
|
|
elif outSlot >= lowSlot:
|
|
|
|
|
# Request is only partially relevant.
|
|
|
|
|
let
|
|
|
|
|
numSlotsDone = highSlot - outSlot
|
2022-06-06 13:56:59 +00:00
|
|
|
|
sr.count -= numSlotsDone
|
2022-03-15 17:56:56 +00:00
|
|
|
|
else:
|
|
|
|
|
# Entire request is no longer relevant.
|
|
|
|
|
sr.count = 0
|
|
|
|
|
|
2021-12-08 21:15:29 +00:00
|
|
|
|
proc pop*[T](sq: SyncQueue[T], maxslot: Slot, item: T): SyncRequest[T] =
|
|
|
|
|
## Create new request according to current SyncQueue parameters.
|
2022-03-15 17:56:56 +00:00
|
|
|
|
sq.handlePotentialSafeSlotAdvancement()
|
|
|
|
|
while len(sq.debtsQueue) > 0:
|
2022-04-08 16:22:49 +00:00
|
|
|
|
if maxslot < sq.debtsQueue[0].slot:
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# Peer's latest slot is less than starting request's slot.
|
|
|
|
|
return SyncRequest.empty(sq.kind, T)
|
2022-04-08 16:22:49 +00:00
|
|
|
|
if maxslot < sq.debtsQueue[0].lastSlot():
|
2021-12-08 21:15:29 +00:00
|
|
|
|
# Peer's latest slot is less than finishing request's slot.
|
|
|
|
|
return SyncRequest.empty(sq.kind, T)
|
|
|
|
|
var sr = sq.debtsQueue.pop()
|
|
|
|
|
sq.debtsCount = sq.debtsCount - sr.count
|
2022-03-15 17:56:56 +00:00
|
|
|
|
sq.updateRequestForNewSafeSlot(sr)
|
|
|
|
|
if sr.isEmpty:
|
|
|
|
|
continue
|
2021-12-08 21:15:29 +00:00
|
|
|
|
sr.setItem(item)
|
|
|
|
|
sq.makePending(sr)
|
2022-03-15 17:56:56 +00:00
|
|
|
|
return sr
|
|
|
|
|
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
2022-04-08 16:22:49 +00:00
|
|
|
|
if maxslot < sq.inpSlot:
|
2022-03-15 17:56:56 +00:00
|
|
|
|
# Peer's latest slot is less than queue's input slot.
|
|
|
|
|
return SyncRequest.empty(sq.kind, T)
|
|
|
|
|
if sq.inpSlot > sq.finalSlot:
|
|
|
|
|
# Queue's input slot is bigger than queue's final slot.
|
|
|
|
|
return SyncRequest.empty(sq.kind, T)
|
|
|
|
|
let lastSlot = min(maxslot, sq.finalSlot)
|
|
|
|
|
let count = min(sq.chunkSize, lastSlot + 1'u64 - sq.inpSlot)
|
|
|
|
|
var sr = SyncRequest.init(sq.kind, sq.inpSlot, count, item)
|
|
|
|
|
sq.advanceInput(count)
|
|
|
|
|
sq.makePending(sr)
|
|
|
|
|
sr
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
if sq.inpSlot == 0xFFFF_FFFF_FFFF_FFFF'u64:
|
|
|
|
|
return SyncRequest.empty(sq.kind, T)
|
|
|
|
|
if sq.inpSlot < sq.finalSlot:
|
|
|
|
|
return SyncRequest.empty(sq.kind, T)
|
|
|
|
|
let (slot, count) =
|
|
|
|
|
block:
|
|
|
|
|
let baseSlot = sq.inpSlot + 1'u64
|
|
|
|
|
if baseSlot - sq.finalSlot < sq.chunkSize:
|
|
|
|
|
let count = uint64(baseSlot - sq.finalSlot)
|
|
|
|
|
(baseSlot - count, count)
|
|
|
|
|
else:
|
|
|
|
|
(baseSlot - sq.chunkSize, sq.chunkSize)
|
2022-04-08 16:22:49 +00:00
|
|
|
|
if (maxslot + 1'u64) < slot + count:
|
2022-03-15 17:56:56 +00:00
|
|
|
|
# Peer's latest slot is less than queue's input slot.
|
|
|
|
|
return SyncRequest.empty(sq.kind, T)
|
|
|
|
|
var sr = SyncRequest.init(sq.kind, slot, count, item)
|
|
|
|
|
sq.advanceInput(count)
|
|
|
|
|
sq.makePending(sr)
|
2021-12-08 21:15:29 +00:00
|
|
|
|
sr
|
|
|
|
|
|
|
|
|
|
proc debtLen*[T](sq: SyncQueue[T]): uint64 =
|
|
|
|
|
sq.debtsCount
|
|
|
|
|
|
|
|
|
|
proc pendingLen*[T](sq: SyncQueue[T]): uint64 =
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
|
|
|
|
# When moving forward `outSlot` will be <= of `inpSlot`.
|
|
|
|
|
sq.inpSlot - sq.outSlot
|
|
|
|
|
of SyncQueueKind.Backward:
|
|
|
|
|
# When moving backward `outSlot` will be >= of `inpSlot`
|
|
|
|
|
sq.outSlot - sq.inpSlot
|
|
|
|
|
|
|
|
|
|
proc len*[T](sq: SyncQueue[T]): uint64 {.inline.} =
|
|
|
|
|
## Returns number of slots left in queue ``sq``.
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
2022-12-23 07:42:55 +00:00
|
|
|
|
if sq.finalSlot >= sq.outSlot:
|
|
|
|
|
sq.finalSlot + 1'u64 - sq.outSlot
|
|
|
|
|
else:
|
|
|
|
|
0'u64
|
2021-12-08 21:15:29 +00:00
|
|
|
|
of SyncQueueKind.Backward:
|
2022-12-23 07:42:55 +00:00
|
|
|
|
if sq.outSlot >= sq.finalSlot:
|
|
|
|
|
sq.outSlot + 1'u64 - sq.finalSlot
|
|
|
|
|
else:
|
|
|
|
|
0'u64
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc total*[T](sq: SyncQueue[T]): uint64 {.inline.} =
|
|
|
|
|
## Returns total number of slots in queue ``sq``.
|
|
|
|
|
case sq.kind
|
|
|
|
|
of SyncQueueKind.Forward:
|
2022-12-23 07:42:55 +00:00
|
|
|
|
if sq.finalSlot >= sq.startSlot:
|
|
|
|
|
sq.finalSlot + 1'u64 - sq.startSlot
|
|
|
|
|
else:
|
|
|
|
|
0'u64
|
2021-12-08 21:15:29 +00:00
|
|
|
|
of SyncQueueKind.Backward:
|
2022-12-23 07:42:55 +00:00
|
|
|
|
if sq.startSlot >= sq.finalSlot:
|
|
|
|
|
sq.startSlot + 1'u64 - sq.finalSlot
|
|
|
|
|
else:
|
|
|
|
|
0'u64
|
2021-12-08 21:15:29 +00:00
|
|
|
|
|
|
|
|
|
proc progress*[T](sq: SyncQueue[T]): uint64 =
|
2022-12-23 07:42:55 +00:00
|
|
|
|
## How many useful slots we've synced so far, adjusting for how much has
|
|
|
|
|
## become obsolete by time movements
|
|
|
|
|
sq.total - sq.len
|