# beacon_chain # 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: [Defect].} import chronicles import options, deques, heapqueue, tables, strutils, sequtils, math, algorithm import stew/results, chronos, chronicles import ../spec/[datatypes/phase0, datatypes/altair, digest, helpers, eth2_apis/callsigs_types, forkedbeaconstate_helpers], ../networking/[peer_pool, eth2_network] import ../gossip_processing/block_processor import ../consensus_object_pools/block_pools_types export phase0, altair, digest, chronos, chronicles, results, block_pools_types, helpers logScope: topics = "syncman" const PeerScoreNoStatus* = -100 ## Peer did not answer `status` request. PeerScoreStaleStatus* = -50 ## Peer's `status` answer do not progress in time. PeerScoreUseless* = -10 ## Peer's latest head is lower then ours. PeerScoreGoodStatus* = 50 ## Peer's `status` answer is fine. PeerScoreNoBlocks* = -100 ## Peer did not respond in time on `blocksByRange` request. PeerScoreGoodBlocks* = 100 ## Peer's `blocksByRange` answer is fine. PeerScoreBadBlocks* = -1000 ## Peer's response contains incorrect blocks. PeerScoreBadResponse* = -1000 ## Peer's response is not in requested range. PeerScoreMissingBlocks* = -200 ## Peer response contains too many empty blocks. SyncWorkersCount* = 10 ## Number of sync workers to spawn StatusUpdateInterval* = chronos.minutes(1) ## Minimum time between two subsequent calls to update peer's status StatusExpirationTime* = chronos.minutes(2) ## Time time it takes for the peer's status information to expire. type SyncFailureKind* = enum StatusInvalid, StatusDownload, StatusStale, EmptyProblem, BlockDownload, BadResponse GetSlotCallback* = proc(): Slot {.gcsafe, raises: [Defect].} SyncRequest*[T] = object index*: uint64 slot*: Slot count*: uint64 step*: uint64 item*: T SyncResult*[T] = object request*: SyncRequest[T] data*: seq[ForkedSignedBeaconBlock] SyncWaiter*[T] = object future: Future[bool] request: SyncRequest[T] RewindPoint = object failSlot: Slot epochCount: uint64 SyncQueue*[T] = ref object inpSlot*: Slot outSlot*: Slot startSlot*: Slot lastSlot: Slot chunkSize*: uint64 queueSize*: int counter*: uint64 opcounter*: uint64 pending*: Table[uint64, SyncRequest[T]] waiters: seq[SyncWaiter[T]] getFinalizedSlot*: GetSlotCallback debtsQueue: HeapQueue[SyncRequest[T]] debtsCount: uint64 readyQueue: HeapQueue[SyncResult[T]] rewind: Option[RewindPoint] blockProcessor: ref BlockProcessor SyncWorkerStatus* {.pure.} = enum Sleeping, WaitingPeer, UpdatingStatus, Requesting, Downloading, Processing SyncWorker*[A, B] = object future: Future[void] status: SyncWorkerStatus SyncManager*[A, B] = ref object pool: PeerPool[A, B] responseTimeout: chronos.Duration sleepTime: chronos.Duration maxStatusAge: uint64 maxHeadAge: uint64 toleranceValue: uint64 getLocalHeadSlot: GetSlotCallback getLocalWallSlot: GetSlotCallback getFinalizedSlot: GetSlotCallback workers: array[SyncWorkersCount, SyncWorker[A, B]] notInSyncEvent: AsyncEvent rangeAge: uint64 inRangeEvent*: AsyncEvent notInRangeEvent*: AsyncEvent chunkSize: uint64 queue: SyncQueue[A] syncFut: Future[void] blockProcessor: ref BlockProcessor inProgress*: bool insSyncSpeed*: float avgSyncSpeed*: float timeLeft*: Duration syncCount*: uint64 syncStatus*: string SyncMoment* = object stamp*: chronos.Moment slot*: Slot SyncFailure*[T] = object kind*: SyncFailureKind peer*: T stamp*: chronos.Moment SyncManagerError* = object of CatchableError BeaconBlocksRes* = NetRes[seq[ForkedSignedBeaconBlock]] proc validate*[T](sq: SyncQueue[T], blk: ForkedSignedBeaconBlock): Future[Result[void, BlockError]] = let resfut = newFuture[Result[void, BlockError]]("sync.manager.validate") sq.blockProcessor[].addBlock(blk, resfut) resfut proc getShortMap*[T](req: SyncRequest[T], data: openArray[ForkedSignedBeaconBlock]): string = ## 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): if slider == data[k].slot: res.add('x') last = k + 1 break elif slider < data[k].slot: res.add('.') break else: res.add('.') slider = slider + req.step result = res proc contains*[T](req: SyncRequest[T], slot: Slot): bool {.inline.} = slot >= req.slot and slot < req.slot + req.count * req.step and ((slot - req.slot) mod req.step == 0) proc cmp*[T](a, b: SyncRequest[T]): int = result = cmp(uint64(a.slot), uint64(b.slot)) proc checkResponse*[T](req: SyncRequest[T], data: openArray[ForkedSignedBeaconBlock]): bool = 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)): if slot < data[dindex].slot: discard elif slot == data[dindex].slot: inc(dindex) else: return false slot = slot + req.step rindex = rindex + 1'u64 if dindex == len(data): return true else: return false proc getFullMap*[T](req: SyncRequest[T], data: openArray[ForkedSignedBeaconBlock]): string = # Returns all slot numbers in ``data`` as comma-delimeted string. result = mapIt(data, $it.message.slot).join(", ") proc init*[T](t1: typedesc[SyncRequest], t2: typedesc[T], slot: Slot, count: uint64): SyncRequest[T] = result = SyncRequest[T](slot: slot, count: count, step: 1'u64) proc init*[T](t1: typedesc[SyncRequest], t2: typedesc[T], start: Slot, finish: Slot): SyncRequest[T] = let count = finish - start + 1'u64 result = SyncRequest[T](slot: start, count: count, step: 1'u64) proc init*[T](t1: typedesc[SyncRequest], t2: typedesc[T], slot: Slot, count: uint64, item: T): SyncRequest[T] = result = SyncRequest[T](slot: slot, count: count, item: item, step: 1'u64) proc init*[T](t1: typedesc[SyncRequest], t2: typedesc[T], start: Slot, finish: Slot, item: T): SyncRequest[T] = let count = finish - start + 1'u64 result = SyncRequest[T](slot: start, count: count, step: 1'u64, item: item) proc init*[T](t1: typedesc[SyncFailure], kind: SyncFailureKind, peer: T): SyncFailure[T] = result = SyncFailure[T](kind: kind, peer: peer, stamp: now(chronos.Moment)) proc empty*[T](t: typedesc[SyncRequest], t2: typedesc[T]): SyncRequest[T] {.inline.} = result = SyncRequest[T](step: 0'u64, count: 0'u64) proc setItem*[T](sr: var SyncRequest[T], item: T) = sr.item = item proc isEmpty*[T](sr: SyncRequest[T]): bool {.inline.} = result = (sr.step == 0'u64) and (sr.count == 0'u64) proc init*[T](t1: typedesc[SyncQueue], t2: typedesc[T], start, last: Slot, chunkSize: uint64, getFinalizedSlotCb: GetSlotCallback, blockProcessor: ref BlockProcessor, syncQueueSize: int = -1): SyncQueue[T] = ## Create new synchronization queue with parameters ## ## ``start`` and ``last`` are starting and finishing Slots. ## ## ``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). ## ## ``updateCb`` procedure which will be used to send downloaded blocks to ## consumer. Procedure should return ``false`` only when it receives ## incorrect blocks, and ``true`` if sequence of blocks is correct. # 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 # # According to current Ethereum2 network specification # > 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. # # 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 - # # If peer answers with `1` everything will be fine and `block_pool` will be # able to process all 3 blocks. In case of `2`, `3`, `4`, `6` - `block_pool` # will fail immediately with chunk and report "parent is missing" error. # But in case of `5` and `7` blocks will be processed by `block_pool` without # any problems, however it will start producing problems right from this # uncertain last slot. SyncQueue will start producing requests for next # 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. doAssert(chunkSize > 0'u64, "Chunk size should not be zero") result = SyncQueue[T]( startSlot: start, lastSlot: last, chunkSize: chunkSize, queueSize: syncQueueSize, getFinalizedSlot: getFinalizedSlotCb, waiters: newSeq[SyncWaiter[T]](), counter: 1'u64, pending: initTable[uint64, SyncRequest[T]](), debtsQueue: initHeapQueue[SyncRequest[T]](), inpSlot: start, outSlot: start, blockProcessor: blockProcessor ) proc `<`*[T](a, b: SyncRequest[T]): bool {.inline.} = result = (a.slot < b.slot) proc `<`*[T](a, b: SyncResult[T]): bool {.inline.} = result = (a.request.slot < b.request.slot) proc `==`*[T](a, b: SyncRequest[T]): bool {.inline.} = result = ((a.slot == b.slot) and (a.count == b.count) and (a.step == b.step)) proc lastSlot*[T](req: SyncRequest[T]): Slot {.inline.} = ## Returns last slot for request ``req``. result = 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``. doAssert(sq.lastSlot <= last, "Last slot could not be lower then stored one " & $sq.lastSlot & " <= " & $last) sq.lastSlot = last proc wakeupWaiters[T](sq: SyncQueue[T], flag = true) = ## Wakeup one or all blocked waiters. for item in sq.waiters: if not(item.future.finished()): item.future.complete(flag) proc waitForChanges[T](sq: SyncQueue[T], req: SyncRequest[T]): Future[bool] {.async.} = ## Create new waiter and wait for completion from `wakeupWaiters()`. var waitfut = newFuture[bool]("SyncQueue.waitForChanges") let waititem = SyncWaiter[T](future: waitfut, request: req) sq.waiters.add(waititem) try: result = await waitfut finally: sq.waiters.delete(sq.waiters.find(waititem)) proc wakeupAndWaitWaiters[T](sq: SyncQueue[T]) {.async.} = ## This procedure will perform wakeupWaiters(false) and blocks until last ## waiter will be awakened. var waitChanges = sq.waitForChanges(SyncRequest.empty(T)) sq.wakeupWaiters(false) discard await waitChanges 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 ## call wakeupWaiters(false). Because our waiter is last in sequence of ## 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. var minSlot = sq.outSlot if toSlot.isSome(): minSlot = min(toSlot.get(), 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 if sr.data[^1].slot != lastslot: 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: sr.data[^1].slot 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, finalizedSlot: Slot): Slot = # Calculate the latest finalized epoch. let finalizedEpoch = compute_epoch_at_slot(finalizedSlot) # Calculate failure epoch. let failEpoch = compute_epoch_at_slot(failSlot) # 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 = finalizedSlot, fail_slot = failSlot, finalized_epoch = finalizedEpoch, fail_epoch = failEpoch, rewind_epoch_count = rewind.epochCount, finalized_epoch = finalizedEpoch 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 = finalizedSlot, fail_slot = failSlot, finalized_epoch = finalizedEpoch, fail_epoch = failEpoch, rewind_epoch_count = rewind.epochCount, finalized_epoch = finalizedEpoch 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 = finalizedSlot, fail_slot = failSlot, finalized_epoch = finalizedEpoch, fail_epoch = failEpoch, finalized_epoch = finalizedEpoch 0'u64 else: 1'u64 # echo "epochCount = ", epochCount if epochCount == 0'u64: warn "Unable to continue syncing, please restart the node", finalized_slot = finalizedSlot, fail_slot = failSlot, finalized_epoch = finalizedEpoch, fail_epoch = failEpoch, finalized_epoch = finalizedEpoch # Calculate the rewind epoch, which will be equal to last rewind point or # finalizedEpoch let rewindEpoch = if sq.rewind.isNone(): finalizedEpoch else: compute_epoch_at_slot(sq.rewind.get().failSlot) - sq.rewind.get().epochCount compute_start_slot_at_epoch(rewindEpoch) 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)) compute_start_slot_at_epoch(rewindEpoch) proc push*[T](sq: SyncQueue[T], sr: SyncRequest[T], data: seq[ForkedSignedBeaconBlock]) {.async, gcsafe.} = ## Push successfull result to queue ``sq``. mixin updateScore 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 # all pending `push` requests if `request.slot` not in range: # [current_queue_slot, current_queue_slot + sq.queueSize * sq.chunkSize]. var exitNow = false while true: if (sq.queueSize > 0) and (sr.slot >= sq.outSlot + uint64(sq.queueSize) * sq.chunkSize): let res = await sq.waitForChanges(sr) if res: continue else: # SyncQueue reset happens. We are exiting to wake up sync-worker. exitNow = true break let syncres = SyncResult[T](request: sr, data: data) sq.readyQueue.push(syncres) exitNow = false break if exitNow: return while len(sq.readyQueue) > 0: let minSlot = sq.readyQueue[0].request.slot if sq.outSlot != minSlot: break let item = sq.readyQueue.pop() # Validating received blocks one by one var res: Result[void, BlockError] var failSlot: Option[Slot] if len(item.data) > 0: for blk in item.data: trace "Pushing block", block_root = blk.root, block_slot = blk.slot res = await sq.validate(blk) if not(res.isOk): failSlot = some(blk.slot) break else: res = Result[void, BlockError].ok() # Increase progress counter, so watch task will be able to know that we are # not stuck. inc(sq.opcounter) if res.isOk: sq.outSlot = sq.outSlot + item.request.count if len(item.data) > 0: # If there no error and response was not empty we should reward peer # with some bonus score. item.request.item.updateScore(PeerScoreGoodBlocks) sq.wakeupWaiters() else: debug "Block pool rejected peer's response", peer = item.request.item, request_slot = item.request.slot, request_count = item.request.count, request_step = item.request.step, blocks_map = getShortMap(item.request, item.data), blocks_count = len(item.data), errCode = res.error, topics = "syncman" var resetSlot: Option[Slot] if res.error == BlockError.MissingParent: # If we got `BlockError.MissingParent` it means that peer returns chain # of blocks with holes or `block_pool` is in incomplete state. We going # to rewind to the first slot at latest finalized epoch. let req = item.request let finalizedSlot = sq.getFinalizedSlot() if finalizedSlot < req.slot: let rewindSlot = sq.getRewindPoint(failSlot.get(), finalizedSlot) warn "Unexpected missing parent, rewind happens", peer = req.item, rewind_to_slot = rewindSlot, rewind_epoch_count = sq.rewind.get().epochCount, rewind_fail_slot = failSlot.get(), finalized_slot = finalized_slot, request_slot = req.slot, request_count = req.count, request_step = req.step, blocks_count = len(item.data), blocks_map = getShortMap(req, item.data), topics = "syncman" resetSlot = some(rewindSlot) req.item.updateScore(PeerScoreMissingBlocks) else: error "Unexpected missing parent at finalized epoch slot", peer = req.item, to_slot = finalizedSlot, request_slot = req.slot, request_count = req.count, request_step = req.step, blocks_count = len(item.data), blocks_map = getShortMap(req, item.data), topics = "syncman" req.item.updateScore(PeerScoreBadBlocks) elif res.error == BlockError.Invalid: let req = item.request warn "Received invalid sequence of blocks", peer = req.item, request_slot = req.slot, request_count = req.count, request_step = req.step, blocks_count = len(item.data), blocks_map = getShortMap(req, item.data), topics = "syncman" req.item.updateScore(PeerScoreBadBlocks) else: let req = item.request warn "Received unexpected response from block_pool", peer = req.item, request_slot = req.slot, request_count = req.count, request_step = req.step, blocks_count = len(item.data), blocks_map = getShortMap(req, item.data), errorCode = res.error, topics = "syncman" req.item.updateScore(PeerScoreBadBlocks) # We need to move failed response to the debts queue. sq.toDebtsQueue(item.request) if resetSlot.isSome(): await sq.resetWait(resetSlot) debug "Rewind to slot was happened", reset_slot = reset_slot.get(), queue_input_slot = sq.inpSlot, queue_output_slot = sq.outSlot, rewind_epoch_count = sq.rewind.get().epochCount, rewind_fail_slot = sq.rewind.get().failSlot, reset_slot = resetSlot, topics = "syncman" 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) proc pop*[T](sq: SyncQueue[T], maxslot: Slot, item: T): SyncRequest[T] = if len(sq.debtsQueue) > 0: if maxSlot < sq.debtsQueue[0].slot: return SyncRequest.empty(T) var sr = sq.debtsQueue.pop() if sr.lastSlot() <= maxSlot: sq.debtsCount = sq.debtsCount - sr.count sr.setItem(item) sq.makePending(sr) return sr var sr1 = SyncRequest.init(T, sr.slot, maxslot, item) let sr2 = SyncRequest.init(T, maxslot + 1'u64, sr.lastSlot()) sq.debtsQueue.push(sr2) sq.debtsCount = sq.debtsCount - sr1.count sq.makePending(sr1) return sr1 else: if maxSlot < sq.inpSlot: return SyncRequest.empty(T) if sq.inpSlot > sq.lastSlot: return SyncRequest.empty(T) let lastSlot = min(maxslot, sq.lastSlot) let count = min(sq.chunkSize, lastSlot + 1'u64 - sq.inpSlot) var sr = SyncRequest.init(T, sq.inpSlot, count, item) sq.inpSlot = sq.inpSlot + count sq.makePending(sr) return sr proc len*[T](sq: SyncQueue[T]): uint64 {.inline.} = ## Returns number of slots left in queue ``sq``. if sq.inpSlot > sq.lastSlot: result = sq.debtsCount else: result = sq.lastSlot - sq.inpSlot + 1'u64 - sq.debtsCount proc total*[T](sq: SyncQueue[T]): uint64 {.inline.} = ## Returns total number of slots in queue ``sq``. result = sq.lastSlot - sq.startSlot + 1'u64 proc progress*[T](sq: SyncQueue[T]): uint64 = ## Returns queue's ``sq`` progress string. let curSlot = sq.outSlot - sq.startSlot result = (curSlot * 100'u64) div sq.total() proc now*(sm: typedesc[SyncMoment], slot: Slot): SyncMoment {.inline.} = result = SyncMoment(stamp: now(chronos.Moment), slot: slot) proc speed*(start, finish: SyncMoment): float {.inline.} = ## Returns number of slots per second. let slots = finish.slot - start.slot let dur = finish.stamp - start.stamp let secs = float(chronos.seconds(1).nanoseconds) if isZero(dur): result = 0.0 else: let v = float(slots) * (secs / float(dur.nanoseconds)) # We doing round manually because stdlib.round is deprecated result = round(v * 10000) / 10000 proc newSyncManager*[A, B](pool: PeerPool[A, B], getLocalHeadSlotCb: GetSlotCallback, getLocalWallSlotCb: GetSlotCallback, getFinalizedSlotCb: GetSlotCallback, blockProcessor: ref BlockProcessor, maxStatusAge = uint64(SLOTS_PER_EPOCH * 4), maxHeadAge = uint64(SLOTS_PER_EPOCH * 1), sleepTime = (int(SLOTS_PER_EPOCH) * int(SECONDS_PER_SLOT)).seconds, chunkSize = uint64(SLOTS_PER_EPOCH), toleranceValue = uint64(1), rangeAge = uint64(SLOTS_PER_EPOCH * 4) ): SyncManager[A, B] = let queue = SyncQueue.init(A, getLocalHeadSlotCb(), getLocalWallSlotCb(), chunkSize, getFinalizedSlotCb, blockProcessor, 1) result = SyncManager[A, B]( pool: pool, maxStatusAge: maxStatusAge, getLocalHeadSlot: getLocalHeadSlotCb, getLocalWallSlot: getLocalWallSlotCb, getFinalizedSlot: getFinalizedSlotCb, maxHeadAge: maxHeadAge, sleepTime: sleepTime, chunkSize: chunkSize, queue: queue, blockProcessor: blockProcessor, notInSyncEvent: newAsyncEvent(), inRangeEvent: newAsyncEvent(), notInRangeEvent: newAsyncEvent(), rangeAge: rangeAge ) proc getBlocks*[A, B](man: SyncManager[A, B], peer: A, req: SyncRequest): Future[BeaconBlocksRes] {.async.} = mixin beaconBlocksByRange, getScore, `==` doAssert(not(req.isEmpty()), "Request must not be empty!") debug "Requesting blocks from peer", peer = peer, slot = req.slot, slot_count = req.count, step = req.step, peer_score = peer.getScore(), peer_speed = peer.netKbps(), topics = "syncman" if peer.useSyncV2(): var workFut = awaitne beaconBlocksByRange_v2(peer, req.slot, req.count, req.step) if workFut.failed(): debug "Error, while waiting getBlocks response", peer = peer, slot = req.slot, slot_count = req.count, step = req.step, errMsg = workFut.readError().msg, peer_speed = peer.netKbps(), topics = "syncman" else: let res = workFut.read() if res.isErr: debug "Error, while reading getBlocks response", peer = peer, slot = req.slot, count = req.count, step = req.step, peer_speed = peer.netKbps(), topics = "syncman", error = $res.error() result = res else: var workFut = awaitne beaconBlocksByRange(peer, req.slot, req.count, req.step) if workFut.failed(): debug "Error, while waiting getBlocks response", peer = peer, slot = req.slot, slot_count = req.count, step = req.step, errMsg = workFut.readError().msg, peer_speed = peer.netKbps(), topics = "syncman" else: let res = workFut.read() if res.isErr: debug "Error, while reading getBlocks response", peer = peer, slot = req.slot, count = req.count, step = req.step, peer_speed = peer.netKbps(), topics = "syncman", error = $res.error() result = res.map() do (blcks: seq[phase0.SignedBeaconBlock]) -> auto: blcks.mapIt(ForkedSignedBeaconBlock.init(it)) template headAge(): uint64 = wallSlot - headSlot template queueAge(): uint64 = wallSlot - man.queue.outSlot template peerStatusAge(): Duration = Moment.now() - peer.state(BeaconSync).statusLastTime func syncQueueLen*[A, B](man: SyncManager[A, B]): uint64 = man.queue.len proc syncStep[A, B](man: SyncManager[A, B], index: int, peer: A) {.async.} = let wallSlot = man.getLocalWallSlot() let headSlot = man.getLocalHeadSlot() var peerSlot = peer.getHeadSlot() # We updating SyncQueue's last slot all the time man.queue.updateLastSlot(wallSlot) debug "Peer's syncing status", wall_clock_slot = wallSlot, remote_head_slot = peerSlot, local_head_slot = headSlot, peer_score = peer.getScore(), peer = peer, index = index, peer_speed = peer.netKbps(), topics = "syncman" # Check if peer's head slot is bigger than our wall clock slot. if peerSlot > wallSlot + man.toleranceValue: warn "Local timer is broken or peer's status information is invalid", wall_clock_slot = wallSlot, remote_head_slot = peerSlot, local_head_slot = headSlot, peer = peer, index = index, tolerance_value = man.toleranceValue, peer_speed = peer.netKbps(), peer_score = peer.getScore(), topics = "syncman" discard SyncFailure.init(SyncFailureKind.StatusInvalid, peer) return # Check if we need to update peer's status information if peerStatusAge >= StatusExpirationTime: # Peer's status information is very old, its time to update it man.workers[index].status = SyncWorkerStatus.UpdatingStatus trace "Updating peer's status information", wall_clock_slot = wallSlot, remote_head_slot = peerSlot, local_head_slot = headSlot, peer = peer, peer_score = peer.getScore(), index = index, peer_speed = peer.netKbps(), topics = "syncman" try: let res = await peer.updateStatus() if not(res): peer.updateScore(PeerScoreNoStatus) debug "Failed to get remote peer's status, exiting", peer = peer, peer_score = peer.getScore(), peer_head_slot = peerSlot, peer_speed = peer.netKbps(), index = index, topics = "syncman" discard SyncFailure.init(SyncFailureKind.StatusDownload, peer) return except CatchableError as exc: debug "Unexpected exception while updating peer's status", peer = peer, peer_score = peer.getScore(), peer_head_slot = peerSlot, peer_speed = peer.netKbps(), index = index, errMsg = exc.msg, topics = "syncman" return let newPeerSlot = peer.getHeadSlot() if peerSlot >= newPeerSlot: peer.updateScore(PeerScoreStaleStatus) debug "Peer's status information is stale", wall_clock_slot = wallSlot, remote_old_head_slot = peerSlot, local_head_slot = headSlot, remote_new_head_slot = newPeerSlot, peer = peer, peer_score = peer.getScore(), index = index, peer_speed = peer.netKbps(), topics = "syncman" else: debug "Peer's status information updated", wall_clock_slot = wallSlot, remote_old_head_slot = peerSlot, local_head_slot = headSlot, remote_new_head_slot = newPeerSlot, peer = peer, peer_score = peer.getScore(), peer_speed = peer.netKbps(), index = index, topics = "syncman" peer.updateScore(PeerScoreGoodStatus) peerSlot = newPeerSlot if headAge <= man.maxHeadAge: info "We are in sync with network", wall_clock_slot = wallSlot, remote_head_slot = peerSlot, local_head_slot = headSlot, peer = peer, peer_score = peer.getScore(), index = index, peer_speed = peer.netKbps(), topics = "syncman" # We clear SyncManager's `notInSyncEvent` so all the workers will become # sleeping soon. man.notInSyncEvent.clear() return if headSlot >= peerSlot - man.maxHeadAge: debug "We are in sync with peer; refreshing peer's status information", wall_clock_slot = wallSlot, remote_head_slot = peerSlot, local_head_slot = headSlot, peer = peer, peer_score = peer.getScore(), index = index, peer_speed = peer.netKbps(), topics = "syncman" man.workers[index].status = SyncWorkerStatus.UpdatingStatus if peerStatusAge <= StatusUpdateInterval: await sleepAsync(StatusUpdateInterval - peerStatusAge) try: let res = await peer.updateStatus() if not(res): peer.updateScore(PeerScoreNoStatus) debug "Failed to get remote peer's status, exiting", peer = peer, peer_score = peer.getScore(), peer_head_slot = peerSlot, peer_speed = peer.netKbps(), index = index, topics = "syncman" discard SyncFailure.init(SyncFailureKind.StatusDownload, peer) return except CatchableError as exc: debug "Unexpected exception while updating peer's status", peer = peer, peer_score = peer.getScore(), peer_head_slot = peerSlot, peer_speed = peer.netKbps(), index = index, errMsg = exc.msg, topics = "syncman" return let newPeerSlot = peer.getHeadSlot() if peerSlot >= newPeerSlot: peer.updateScore(PeerScoreStaleStatus) debug "Peer's status information is stale", wall_clock_slot = wallSlot, remote_old_head_slot = peerSlot, local_head_slot = headSlot, remote_new_head_slot = newPeerSlot, peer = peer, peer_score = peer.getScore(), index = index, peer_speed = peer.netKbps(), topics = "syncman" else: # This is not very good solution because we should not discriminate and/or # penalize peers which are in sync process too, but their latest head is # lower then our latest head. We should keep connections with such peers # (so this peers are able to get in sync using our data), but we should # not use this peers for syncing because this peers are useless for us. # Right now we decreasing peer's score a bit, so it will not be # disconnected due to low peer's score, but new fresh peers could replace # peers with low latest head. if headSlot >= newPeerSlot - man.maxHeadAge: # Peer's head slot is still lower then ours. debug "Peer's head slot is lower then local head slot", wall_clock_slot = wallSlot, remote_old_head_slot = peerSlot, local_head_slot = headSlot, remote_new_head_slot = newPeerSlot, peer = peer, peer_score = peer.getScore(), peer_speed = peer.netKbps(), index = index, topics = "syncman" peer.updateScore(PeerScoreUseless) else: debug "Peer's status information updated", wall_clock_slot = wallSlot, remote_old_head_slot = peerSlot, local_head_slot = headSlot, remote_new_head_slot = newPeerSlot, peer = peer, peer_score = peer.getScore(), peer_speed = peer.netKbps(), index = index, topics = "syncman" peer.updateScore(PeerScoreGoodStatus) peerSlot = newPeerSlot return man.workers[index].status = SyncWorkerStatus.Requesting let req = man.queue.pop(peerSlot, peer) if req.isEmpty(): # SyncQueue could return empty request in 2 cases: # 1. There no more slots in SyncQueue to download (we are synced, but # our ``notInSyncEvent`` is not yet cleared). # 2. Current peer's known head slot is too low to satisfy request. # # To avoid endless loop we going to wait for RESP_TIMEOUT time here. # This time is enough for all pending requests to finish and it is also # enough for main sync loop to clear ``notInSyncEvent``. debug "Empty request received from queue, exiting", peer = peer, local_head_slot = headSlot, remote_head_slot = peerSlot, queue_input_slot = man.queue.inpSlot, queue_output_slot = man.queue.outSlot, queue_last_slot = man.queue.lastSlot, peer_speed = peer.netKbps(), peer_score = peer.getScore(), index = index, topics = "syncman" await sleepAsync(RESP_TIMEOUT) return debug "Creating new request for peer", wall_clock_slot = wallSlot, remote_head_slot = peerSlot, local_head_slot = headSlot, request_slot = req.slot, request_count = req.count, request_step = req.step, peer = peer, peer_speed = peer.netKbps(), peer_score = peer.getScore(), index = index, topics = "syncman" man.workers[index].status = SyncWorkerStatus.Downloading try: let blocks = await man.getBlocks(peer, req) if blocks.isOk: let data = blocks.get() let smap = getShortMap(req, data) debug "Received blocks on request", blocks_count = len(data), blocks_map = smap, request_slot = req.slot, request_count = req.count, request_step = req.step, peer = peer, peer_score = peer.getScore(), peer_speed = peer.netKbps(), index = index, topics = "syncman" if not(checkResponse(req, data)): peer.updateScore(PeerScoreBadResponse) warn "Received blocks sequence is not in requested range", blocks_count = len(data), blocks_map = smap, request_slot = req.slot, request_count = req.count, request_step = req.step, peer = peer, peer_score = peer.getScore(), peer_speed = peer.netKbps(), index = index, topics = "syncman" discard SyncFailure.init(SyncFailureKind.BadResponse, peer) return # Scoring will happen in `syncUpdate`. man.workers[index].status = SyncWorkerStatus.Processing await man.queue.push(req, data) else: peer.updateScore(PeerScoreNoBlocks) man.queue.push(req) debug "Failed to receive blocks on request", request_slot = req.slot, request_count = req.count, request_step = req.step, peer = peer, index = index, peer_score = peer.getScore(), peer_speed = peer.netKbps(), topics = "syncman" discard SyncFailure.init(SyncFailureKind.BlockDownload, peer) return except CatchableError as exc: debug "Unexpected exception while receiving blocks", request_slot = req.slot, request_count = req.count, request_step = req.step, peer = peer, index = index, peer_score = peer.getScore(), peer_speed = peer.netKbps(), errMsg = exc.msg, topics = "syncman" return proc syncWorker[A, B](man: SyncManager[A, B], index: int) {.async.} = mixin getKey, getScore, getHeadSlot debug "Starting syncing worker", index = index, topics = "syncman" while true: man.workers[index].status = SyncWorkerStatus.Sleeping # This event is going to be set until we are not in sync with network await man.notInSyncEvent.wait() man.workers[index].status = SyncWorkerStatus.WaitingPeer let peer = await man.pool.acquire() await man.syncStep(index, peer) man.pool.release(peer) proc getWorkersStats[A, B](man: SyncManager[A, B]): tuple[map: string, sleeping: int, waiting: int, pending: int] = var map = newString(len(man.workers)) var sleeping, waiting, pending: int for i in 0 ..< len(man.workers): var ch: char case man.workers[i].status of SyncWorkerStatus.Sleeping: ch = 's' inc(sleeping) of SyncWorkerStatus.WaitingPeer: ch = 'w' inc(waiting) of SyncWorkerStatus.UpdatingStatus: ch = 'U' inc(pending) of SyncWorkerStatus.Requesting: ch = 'R' inc(pending) of SyncWorkerStatus.Downloading: ch = 'D' inc(pending) of SyncWorkerStatus.Processing: ch = 'P' inc(pending) map[i] = ch (map, sleeping, waiting, pending) proc guardTask[A, B](man: SyncManager[A, B]) {.async.} = var pending: array[SyncWorkersCount, Future[void]] # Starting all the synchronization workers. for i in 0 ..< len(man.workers): let future = syncWorker[A, B](man, i) man.workers[i].future = future pending[i] = future # Wait for synchronization worker's failure and replace it with new one. while true: let failFuture = await one(pending) let index = pending.find(failFuture) if failFuture.failed(): warn "Synchronization worker stopped working unexpectedly with an error", index = index, errMsg = failFuture.error.msg else: warn "Synchronization worker stopped working unexpectedly without error", index = index let future = syncWorker[A, B](man, index) man.workers[index].future = future pending[index] = future proc toTimeLeftString(d: Duration): string = var v = d var res = "" let ndays = chronos.days(v) if ndays > 0: res = res & (if ndays < 10: "0" & $ndays else: $ndays) & "d" v = v - chronos.days(ndays) let nhours = chronos.hours(v) if nhours > 0: res = res & (if nhours < 10: "0" & $nhours else: $nhours) & "h" v = v - chronos.hours(nhours) else: res = res & "00h" let nmins = chronos.minutes(v) if nmins > 0: res = res & (if nmins < 10: "0" & $nmins else: $nmins) & "m" v = v - chronos.minutes(nmins) else: res = res & "00m" res proc syncLoop[A, B](man: SyncManager[A, B]) {.async.} = mixin getKey, getScore var pauseTime = 0 asyncSpawn man.guardTask() debug "Synchronization loop started", topics = "syncman" proc averageSpeedTask() {.async.} = while true: let wallSlot = man.getLocalWallSlot() let headSlot = man.getLocalHeadSlot() let lsm1 = SyncMoment.now(man.getLocalHeadSlot()) await sleepAsync(chronos.seconds(int(SECONDS_PER_SLOT))) let lsm2 = SyncMoment.now(man.getLocalHeadSlot()) let bps = if lsm2.slot - lsm1.slot == 0'u64: 0.0 else: speed(lsm1, lsm2) inc(man.syncCount) man.insSyncSpeed = bps man.avgSyncSpeed = man.avgSyncSpeed + (bps - man.avgSyncSpeed) / float(man.syncCount) let nsec = (float(wallSlot - headSlot) / man.avgSyncSpeed) * 1_000_000_000.0 man.timeLeft = chronos.nanoseconds(int64(nsec)) asyncSpawn averageSpeedTask() while true: let wallSlot = man.getLocalWallSlot() let headSlot = man.getLocalHeadSlot() let (map, sleeping, waiting, pending) = man.getWorkersStats() debug "Current syncing state", workers_map = map, sleeping_workers_count = sleeping, waiting_workers_count = waiting, pending_workers_count = pending, wall_head_slot = wallSlot, local_head_slot = headSlot, pause_time = $chronos.seconds(pauseTime), avg_sync_speed = man.avgSyncSpeed, ins_sync_speed = man.insSyncSpeed, topics = "syncman" # Update status string man.syncStatus = map & ":" & $pending & ":" & man.insSyncSpeed.formatBiggestFloat(ffDecimal, 4) & ":" & man.avgSyncSpeed.formatBiggestFloat(ffDecimal, 4) & ":" & man.timeLeft.toTimeLeftString() & " (" & $man.queue.outSlot & ")" if headAge <= man.maxHeadAge: man.notInSyncEvent.clear() # We are marking SyncManager as not working only when we are in sync and # all sync workers are in `Sleeping` state. if pending > 0: debug "Synchronization loop waits for workers completion", wall_head_slot = wallSlot, local_head_slot = headSlot, difference = (wallSlot - headSlot), max_head_age = man.maxHeadAge, sleeping_workers_count = sleeping, waiting_workers_count = waiting, pending_workers_count = pending, topics = "syncman" man.inProgress = true else: debug "Synchronization loop sleeping", wall_head_slot = wallSlot, local_head_slot = headSlot, difference = (wallSlot - headSlot), max_head_age = man.maxHeadAge, topics = "syncman" man.inProgress = false else: if not(man.notInSyncEvent.isSet()): # We get here only if we lost sync for more then `maxHeadAge` period. if pending == 0: man.queue = SyncQueue.init(A, man.getLocalHeadSlot(), man.getLocalWallSlot(), man.chunkSize, man.getFinalizedSlot, man.blockProcessor, 1) man.notInSyncEvent.fire() man.inProgress = true else: man.notInSyncEvent.fire() man.inProgress = true if queueAge <= man.rangeAge: # We are in requested range ``man.rangeAge``. man.inRangeEvent.fire() man.notInRangeEvent.clear() else: # We are not in requested range anymore ``man.rangeAge``. man.inRangeEvent.clear() man.notInRangeEvent.fire() await sleepAsync(chronos.seconds(2)) proc start*[A, B](man: SyncManager[A, B]) = ## Starts SyncManager's main loop. man.syncFut = man.syncLoop() proc getInfo*[A, B](man: SyncManager[A, B]): SyncInfo = ## Returns current synchronization information for RPC call. let wallSlot = man.getLocalWallSlot() let headSlot = man.getLocalHeadSlot() let sync_distance = wallSlot - headSlot ( head_slot: headSlot, sync_distance: sync_distance, is_syncing: man.inProgress )