UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

650 lines 31.4 kB
import { ErrorAborted, prettyPrintIndices, toRootHex } from "@lodestar/utils"; import { isBlockInputBlobs, isBlockInputColumns } from "../../chain/blocks/blockInput/blockInput.js"; import { BlockInputErrorCode } from "../../chain/blocks/blockInput/errors.js"; import { BlobSidecarErrorCode } from "../../chain/errors/blobSidecarError.js"; import { DataColumnSidecarErrorCode } from "../../chain/errors/dataColumnSidecarError.js"; import { PeerAction, prettyPrintPeerIdStr } from "../../network/index.js"; import { ItTrigger } from "../../util/itTrigger.js"; import { wrapError } from "../../util/wrapError.js"; import { BATCH_BUFFER_SIZE, EPOCHS_PER_BATCH, MAX_LOOK_AHEAD_EPOCHS } from "../constants.js"; import { DownloadByRangeErrorCode } from "../utils/downloadByRange.js"; import { getRateLimitedUntilMs } from "../utils/rateLimit.js"; import { RangeSyncType } from "../utils/remoteSyncType.js"; import { Batch, BatchError, BatchErrorCode, BatchStatus } from "./batch.js"; import { ChainPeersBalancer, batchStartEpochIsAfterSlot, computeHighestTarget, getBatchSlotRange, getNextBatchToProcess, isSyncChainDone, toArr, toBeDownloadedStartEpoch, validateBatchesStatus, } from "./utils/index.js"; export class SyncChainStartError extends Error { } export { SyncChainStatus }; var SyncChainStatus; (function (SyncChainStatus) { SyncChainStatus["Stopped"] = "Stopped"; SyncChainStatus["Syncing"] = "Syncing"; SyncChainStatus["Done"] = "Done"; SyncChainStatus["Error"] = "Error"; })(SyncChainStatus || (SyncChainStatus = {})); // this global chain id is used to identify the chain over time, increase it every time a new chain is created // a chain type could be Finalized or Head, so it should be appended with this id to make the log unique let nextChainId = 0; /** * Dynamic target sync chain. Peers with multiple targets but with the same syncType are added * through the `addPeer()` hook. * * A chain of blocks that need to be downloaded. Peers who claim to contain the target head * root are grouped into the peer pool and queried for batches when downloading the chain. */ export class SyncChain { /** Short string id to identify this SyncChain in logs */ logId; syncType; /** * Should sync up until this slot, then stop. * Finalized SyncChains have a dynamic target, so if this chain has no peers the target can become null */ target; /** Number of validated epochs. For the SyncRange to prevent switching chains too fast */ validatedEpochs = 0; firstBatchEpoch; /** * The start of the chain segment. Any epoch previous to this one has been validated. * Note: lastEpochWithProcessBlocks` signals the epoch at which 1 or more blocks have been processed * successfully. So that epoch itself may or may not be valid. */ lastEpochWithProcessBlocks; status = SyncChainStatus.Stopped; processChainSegment; downloadByRange; reportPeer; getConnectedPeerSyncMeta; pruneBlockInputs; /** AsyncIterable that guarantees processChainSegment is run only at once at anytime */ batchProcessor = new ItTrigger(); /** Sorted map of batches undergoing some kind of processing. */ batches = new Map(); /** * `true` until the first `Batch` is constructed via `includeNextBatch` */ isFirstBatch = true; peerset = new Map(); /** * Tracks peers that have rate-limited us, mapped to the timestamp (ms) until which we should avoid them. * This is a sync-layer optimization to avoid assigning batches to backed-off peers. * The reqresp SelfRateLimiter independently enforces backoff at the protocol level as a safety net. */ rateLimitedPeers = new Map(); rateLimitBackoffTimeout; logger; config; clock; metrics; custodyConfig; latestBid; constructor(initialBatchEpoch, initialTarget, syncType, fns, modules, latestBid) { const { config, clock, custodyConfig, logger, metrics } = modules; this.firstBatchEpoch = initialBatchEpoch; this.lastEpochWithProcessBlocks = initialBatchEpoch; this.target = initialTarget; this.syncType = syncType; this.processChainSegment = fns.processChainSegment; this.downloadByRange = fns.downloadByRange; this.reportPeer = fns.reportPeer; this.pruneBlockInputs = fns.pruneBlockInputs; this.getConnectedPeerSyncMeta = fns.getConnectedPeerSyncMeta; this.config = config; this.clock = clock; this.metrics = metrics; this.custodyConfig = custodyConfig; this.latestBid = latestBid; this.logger = logger; this.logId = `${syncType}-${nextChainId++}`; if (metrics) { metrics.syncRange.headSyncPeers.addCollect(() => this.scrapeMetrics(metrics)); } // Trigger event on parent class this.sync().then(() => fns.onEnd(null, this.target), (e) => fns.onEnd(e, null)); } /** * Start syncing a new chain or an old one with an existing peer list * In the same call, advance the chain if localFinalizedEpoch > */ startSyncing(localFinalizedEpoch) { switch (this.status) { case SyncChainStatus.Stopped: break; // Ok, continue case SyncChainStatus.Syncing: return; // Skip, already started case SyncChainStatus.Error: case SyncChainStatus.Done: throw new SyncChainStartError(`Attempted to start an ended SyncChain ${this.status}`); } this.status = SyncChainStatus.Syncing; this.logger.debug("SyncChain startSyncing", { localFinalizedEpoch, lastEpochWithProcessBlocks: this.lastEpochWithProcessBlocks, targetSlot: this.target.slot, }); // to avoid dropping local progress, we advance the chain with its batch boundaries. // get the aligned epoch that produces a batch containing the `localFinalizedEpoch` const lastEpochWithProcessBlocksAligned = this.lastEpochWithProcessBlocks + Math.floor((localFinalizedEpoch - this.lastEpochWithProcessBlocks) / EPOCHS_PER_BATCH) * EPOCHS_PER_BATCH; this.advanceChain(lastEpochWithProcessBlocksAligned); // Potentially download new batches and process pending this.triggerBatchDownloader(); this.triggerBatchProcessor(); } /** * Temporarily stop the chain. Will prevent batches from being processed */ stopSyncing() { this.status = SyncChainStatus.Stopped; this.clearRateLimitBackoffTimer(); this.logger.debug("SyncChain stopSyncing", { id: this.logId }); } /** * Permanently remove this chain. Throws the main AsyncIterable */ remove() { this.logger.debug("SyncChain remove", { id: this.logId }); this.clearRateLimitBackoffTimer(); this.batchProcessor.end(new ErrorAborted("SyncChain")); } /** * Add peer to the chain and request batches if active */ addPeer(peer, target) { this.peerset.set(peer, target); this.computeTarget(); this.triggerBatchDownloader(); } /** * Returns true if the peer existed and has been removed * NOTE: The RangeSync will take care of deleting the SyncChain if peers = 0 */ removePeer(peerId) { const deleted = this.peerset.delete(peerId); this.rateLimitedPeers.delete(peerId); this.computeTarget(); return deleted; } /** * Helper to print internal state for debugging when chain gets stuck */ getBatchesState() { return toArr(this.batches).map((batch) => batch.getMetadata()); } get lastValidatedSlot() { // Last epoch of the batch after the last one validated return getBatchSlotRange(this.lastEpochWithProcessBlocks + EPOCHS_PER_BATCH).startSlot - 1; } get isSyncing() { return this.status === SyncChainStatus.Syncing; } get isRemovable() { return this.status === SyncChainStatus.Error || this.status === SyncChainStatus.Done; } get peers() { return this.peerset.size; } getPeers() { return Array.from(this.peerset.keys()); } /** Full debug state for lodestar API */ getDebugState() { return { targetRoot: toRootHex(this.target.root), targetSlot: this.target.slot, syncType: this.syncType, status: this.status, startEpoch: this.lastEpochWithProcessBlocks, peers: this.peers, batches: this.getBatchesState(), }; } computeTarget() { if (this.peerset.size > 0) { const targets = Array.from(this.peerset.values()); this.target = computeHighestTarget(targets); } } /** * Main Promise that handles the sync process. Will resolve when initial sync completes * i.e. when it successfully processes a epoch >= than this chain `targetEpoch` */ async sync() { try { // Start processing batches on demand in strict sequence for await (const _ of this.batchProcessor) { if (this.status !== SyncChainStatus.Syncing) { continue; } // TODO: Consider running this check less often after the sync is well tested validateBatchesStatus(toArr(this.batches)); // Returns true if SyncChain has processed all possible blocks with slot <= target.slot if (isSyncChainDone(toArr(this.batches), this.lastEpochWithProcessBlocks, this.target.slot)) { break; } // Processes the next batch if ready const batch = getNextBatchToProcess(toArr(this.batches)); if (batch) await this.processBatch(batch); } this.status = SyncChainStatus.Done; this.logger.verbose("SyncChain Done", { id: this.logId }); } catch (e) { if (e instanceof ErrorAborted) { return; // Ignore } for (const batch of this.batches.values()) { this.pruneBlockInputs(batch.getBlocks()); } this.status = SyncChainStatus.Error; this.logger.verbose("SyncChain Error", { id: this.logId }, e); // If a batch exceeds it's retry limit, maybe downscore peers. // shouldDownscoreOnBatchError() functions enforces that all BatchErrorCode values are covered if (e instanceof BatchError) { const shouldReportPeer = shouldReportPeerOnBatchError(e.type.code); if (shouldReportPeer) { for (const peer of this.peerset.keys()) { this.reportPeer(peer, shouldReportPeer.action, shouldReportPeer.reason); } } } throw e; } finally { this.clearRateLimitBackoffTimer(); } } /** * Request to process batches if possible */ triggerBatchProcessor() { this.batchProcessor.trigger(); } /** * Request to download batches if possible * Backlogs requests into a single pending request */ triggerBatchDownloader() { try { this.requestBatches(); } catch (e) { // bubble the error up to the main async iterable loop this.batchProcessor.end(e); } } scheduleRateLimitBackoffRetry() { this.clearRateLimitBackoffTimer(); if (this.status !== SyncChainStatus.Syncing || this.rateLimitedPeers.size === 0) { return; } const now = Date.now(); let retryAt = null; for (const [peerId, rateLimitedUntil] of this.rateLimitedPeers.entries()) { if (rateLimitedUntil <= now) { this.rateLimitedPeers.delete(peerId); continue; } retryAt = Math.min(retryAt ?? rateLimitedUntil, rateLimitedUntil); } if (retryAt === null) { return; } this.rateLimitBackoffTimeout = setTimeout(() => { this.rateLimitBackoffTimeout = undefined; this.triggerBatchDownloader(); this.scheduleRateLimitBackoffRetry(); }, Math.max(0, retryAt - now)); } clearRateLimitBackoffTimer() { if (this.rateLimitBackoffTimeout !== undefined) { clearTimeout(this.rateLimitBackoffTimeout); this.rateLimitBackoffTimeout = undefined; } } /** * Attempts to request the next required batches from the peer pool if the chain is syncing. * It will exhaust the peer pool and left over batches until the batch buffer is reached. */ requestBatches() { if (this.status !== SyncChainStatus.Syncing) { return; } const now = Date.now(); const peersSyncInfo = []; for (const [peerId, target] of this.peerset.entries()) { // Skip peers that are currently in rate-limit backoff const rateLimitedUntil = this.rateLimitedPeers.get(peerId); if (rateLimitedUntil !== undefined) { if (now < rateLimitedUntil) { continue; } this.rateLimitedPeers.delete(peerId); } try { peersSyncInfo.push({ ...this.getConnectedPeerSyncMeta(peerId), target }); } catch (e) { this.logger.debug("Failed to get peer sync meta", { peerId }, e); } } const peerBalancer = new ChainPeersBalancer(peersSyncInfo, toArr(this.batches), this.custodyConfig, this.syncType); // Retry download of existing batches for (const batch of this.batches.values()) { if (batch.state.status !== BatchStatus.AwaitingDownload) { continue; } const peer = peerBalancer.bestPeerToRetryBatch(batch); if (peer) { void this.sendBatch(batch, peer); } } // find the next pending batch and request it from the peer let batch = this.includeNextBatch(); while (batch != null) { const peer = peerBalancer.idlePeerForBatch(batch); if (!peer) { // if there is no peer available, we stop requesting batches because next batches will have greater startEpoch with the same sampling groups break; } void this.sendBatch(batch, peer); batch = this.includeNextBatch(); } } /** * Creates the next required batch from the chain. If there are no more batches required, returns `null`. */ includeNextBatch() { const batches = toArr(this.batches); // Only request batches up to the buffer size limit // Note: Don't count batches in the AwaitingValidation state, to prevent stalling sync // if the current processing window is contained in a long range of skip slots. const batchesInBuffer = batches.filter((batch) => { return batch.state.status === BatchStatus.Downloading || batch.state.status === BatchStatus.AwaitingProcessing; }); if (batchesInBuffer.length > BATCH_BUFFER_SIZE) { return null; } // if last processed epoch is n, we don't want to request batches with epoch > n + MAX_LOOK_AHEAD_EPOCHS // we should have enough batches to process in the buffer: n + 1, ..., n + MAX_LOOK_AHEAD_EPOCHS // let's focus on redownloading these batches first because it may have to reach different peers to get enough sampled columns if (batches.length > 0 && Math.max(...batches.map((b) => b.startEpoch)) >= this.lastEpochWithProcessBlocks + MAX_LOOK_AHEAD_EPOCHS) { return null; } // This line decides the starting epoch of the next batch. MUST ensure no duplicate batch for the same startEpoch const startEpoch = toBeDownloadedStartEpoch(batches, this.lastEpochWithProcessBlocks); // Don't request batches beyond the target head slot. The to-be-downloaded batch must be strictly after target.slot if (batchStartEpochIsAfterSlot(startEpoch, this.target.slot)) { return null; } if (this.batches.has(startEpoch)) { this.logger.error("Attempting to add existing Batch to SyncChain", { id: this.logId, startEpoch }); return null; } const batch = new Batch(startEpoch, this.config, this.clock, this.custodyConfig, this.isFirstBatch, // `latestBid` is only meaningful for the first batch's parent-payload check this.isFirstBatch ? this.latestBid : undefined, this.target.slot); this.isFirstBatch = false; this.batches.set(startEpoch, batch); return batch; } /** * Requests the batch assigned to the given id from a given peer. */ async sendBatch(batch, peer) { this.logger.verbose("Downloading batch", { id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId), }); try { batch.startDownloading(peer); // wrapError ensures to never call both batch success() and batch error() const res = await wrapError(this.downloadByRange(peer, batch, this.syncType)); if (res.err) { // There's several known error cases where we want to take action on the peer const errCode = res.err.type?.code; this.metrics?.syncRange.downloadByRange.error.inc({ client: peer.client, code: errCode ?? "UNKNOWN" }); if (this.syncType === RangeSyncType.Finalized) { // For finalized sync, we are stricter with peers as there is no ambiguity about which chain we're syncing. // The below cases indicate the peer may be on a different chain, so are not penalized during head sync. switch (errCode) { case BlockInputErrorCode.MISMATCHED_ROOT_HEX: case DownloadByRangeErrorCode.MISSING_BLOBS: case DownloadByRangeErrorCode.EXTRA_BLOBS: case DownloadByRangeErrorCode.MISSING_COLUMNS: case DownloadByRangeErrorCode.EXTRA_COLUMNS: case BlobSidecarErrorCode.INCORRECT_SIDECAR_COUNT: case BlobSidecarErrorCode.INCORRECT_BLOCK: case DataColumnSidecarErrorCode.INCORRECT_SIDECAR_COUNT: case DataColumnSidecarErrorCode.INCORRECT_BLOCK: this.reportPeer(peer.peerId, PeerAction.LowToleranceError, res.err.message); } } switch (errCode) { case DownloadByRangeErrorCode.EXTRA_BLOCKS: case DownloadByRangeErrorCode.OUT_OF_ORDER_BLOCKS: case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS: case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH: case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT: case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT: case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID: case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH: case DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT: case DataColumnSidecarErrorCode.INCORRECT_KZG_PROOF_COUNT: case DataColumnSidecarErrorCode.INVALID_KZG_PROOF_BATCH: case DataColumnSidecarErrorCode.INCLUSION_PROOF_INVALID: this.reportPeer(peer.peerId, PeerAction.LowToleranceError, res.err.message); } this.logger.verbose("Batch download error", { id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId) }, res.err); const rateLimitedUntilMs = getRateLimitedUntilMs(res.err); if (rateLimitedUntilMs !== null) { // Peer rate-limited us — don't count as a failed download attempt and mark peer for backoff this.rateLimitedPeers.set(peer.peerId, rateLimitedUntilMs); this.scheduleRateLimitBackoffRetry(); batch.downloadingRateLimited(); this.triggerBatchDownloader(); } else { batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS } } else { this.logger.verbose("Batch download success", { id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId), }); this.metrics?.syncRange.downloadByRange.success.inc(); const { warnings, result } = res.result; const { blocks: downloadedBlocks, payloadEnvelopes } = result; const downloadSuccessOutput = batch.downloadingSuccess(peer.peerId, downloadedBlocks, payloadEnvelopes); const logMeta = { blockCount: downloadSuccessOutput.blocks.length, }; if (warnings && warnings.length > 0) { for (const warning of warnings) { this.metrics?.syncRange.downloadByRange.warn.inc({ client: peer.client, code: warning.type.code }); this.logger.debug("Batch downloaded with warning", { id: this.logId, ...batch.getMetadata(), ...logMeta, peer: prettyPrintPeerIdStr(peer.peerId) }, warning); } } for (const block of downloadSuccessOutput.blocks) { if (isBlockInputBlobs(block)) { const blockLogMeta = block.getLogMeta(); const expectedBlobs = typeof blockLogMeta.expectedBlobs === "number" ? blockLogMeta.expectedBlobs : 0; logMeta.expectedBlobCount = (logMeta.expectedBlobCount ?? 0) + expectedBlobs; logMeta.receivedBlobCount = (logMeta.receivedBlobCount ?? 0) + blockLogMeta.receivedBlobs; } else if (isBlockInputColumns(block)) { logMeta.columnCount = (logMeta.columnCount ?? 0) + block.getLogMeta().receivedColumns; } } let logMessage; if (downloadSuccessOutput.status === BatchStatus.AwaitingProcessing) { logMessage = "Finished downloading batch by range"; this.triggerBatchProcessor(); } else { logMessage = "Partially downloaded batch by range. Attempting another round of downloads"; // the flow will continue to call triggerBatchDownloader() below } const blockSlots = downloadSuccessOutput.blocks.map((b) => b.slot); const envelopeSlots = downloadSuccessOutput.payloadEnvelopes ? Array.from(downloadSuccessOutput.payloadEnvelopes.keys()) : null; this.logger.debug(logMessage, { id: this.logId, ...batch.getMetadata(), ...logMeta, blockSlots: prettyPrintIndices(blockSlots), ...(envelopeSlots ? { envelopeSlots: prettyPrintIndices(envelopeSlots) } : {}), peer: prettyPrintPeerIdStr(peer.peerId), }); } // Preemptively request more blocks from peers whilst we process current blocks // // TODO(fulu): why is this second call here. should fall through to the one below the catch block. commenting // for now and will resolve during PR process // this.triggerBatchDownloader(); } catch (e) { // bubble the error up to the main async iterable loop this.batchProcessor.end(e); } // Preemptively request more blocks from peers whilst we process current blocks this.triggerBatchDownloader(); } /** * Sends `batch` to the processor. Note: batch may be empty */ async processBatch(batch) { const { blocks, payloadEnvelopes, peers } = batch.startProcessing(); const logCtx = { id: this.logId, ...batch.getMetadata(), blockCount: blocks.length, blockSlots: prettyPrintIndices(blocks.map((b) => b.slot)), ...(payloadEnvelopes ? { envelopeSlots: prettyPrintIndices(Array.from(payloadEnvelopes.keys())) } : {}), peers: peers.map(prettyPrintPeerIdStr).join(","), }; this.logger.verbose("Processing batch", logCtx); // wrapError ensures to never call both batch success() and batch error() const res = await wrapError(this.processChainSegment(blocks, payloadEnvelopes, this.syncType)); if (!res.err) { batch.processingSuccess(); this.logger.verbose("Processed batch", { ...logCtx, ...batch.getMetadata() }); // If the processed batch is not empty, validate previous AwaitingValidation blocks. if (blocks.length > 0) { this.advanceChain(batch.startEpoch); } // Potentially process next AwaitingProcessing batch this.triggerBatchProcessor(); } else { this.logger.verbose("Batch process error", logCtx, res.err); batch.processingError(res.err); // Throws after MAX_BATCH_PROCESSING_ATTEMPTS // At least one block was successfully verified and imported, so we can be sure all // previous batches are valid and we only need to download the current failed batch. // TODO: Disabled for now // if (res.err instanceof ChainSegmentError && res.err.importedBlocks > 0) { // this.advanceChain(batch.startEpoch); // } // The current batch could not be processed, so either this or previous batches are invalid. // All previous batches (AwaitingValidation) are potentially faulty and marked for retry. // Progress will be drop back to `this.startEpoch` for (const pendingBatch of this.batches.values()) { if (pendingBatch.startEpoch < batch.startEpoch) { this.logger.verbose("Batch validation error", { id: this.logId, ...pendingBatch.getMetadata() }); pendingBatch.validationError(res.err); // Throws after MAX_BATCH_PROCESSING_ATTEMPTS } } } // A batch is no longer in Processing status, queue has an empty spot to download next batch this.triggerBatchDownloader(); } /** * Drops any batches previous to `newLatestValidatedEpoch` and updates the chain boundaries */ advanceChain(newLastEpochWithProcessBlocks) { // make sure this epoch produces an advancement if (newLastEpochWithProcessBlocks <= this.lastEpochWithProcessBlocks) { return; } for (const [batchKey, batch] of this.batches.entries()) { if (batch.startEpoch < newLastEpochWithProcessBlocks) { this.batches.delete(batchKey); this.validatedEpochs += EPOCHS_PER_BATCH; // The last batch attempt is right, all others are wrong. Penalize other peers const attemptOk = batch.validationSuccess(); for (const attempt of batch.failedProcessingAttempts) { if (attempt.hash !== attemptOk.hash) { for (const badAttemptPeer of attempt.peers) { if (attemptOk.peers.find((goodPeer) => goodPeer === badAttemptPeer)) { // The same peer corrected its previous attempt this.reportPeer(badAttemptPeer, PeerAction.MidToleranceError, "SyncChainInvalidBatchSelf"); } else { // A different peer sent an bad batch this.reportPeer(badAttemptPeer, PeerAction.LowToleranceError, "SyncChainInvalidBatchOther"); } } } } } } this.lastEpochWithProcessBlocks = newLastEpochWithProcessBlocks; this.logger.verbose("Advanced chain", { id: this.logId, lastEpochWithProcessBlocks: this.lastEpochWithProcessBlocks, }); } scrapeMetrics(metrics) { const syncPeersMetric = this.syncType === RangeSyncType.Finalized ? metrics.syncRange.finalizedSyncPeers : metrics.syncRange.headSyncPeers; const peersSyncMeta = new Map(); for (const peerId of this.peerset.keys()) { try { peersSyncMeta.set(peerId, this.getConnectedPeerSyncMeta(peerId)); } catch (_) { // ignore for metric as peer could be disconnected } } const peersByColumnIndex = new Map(); for (const [columnIndex, column] of this.custodyConfig.sampledColumns.entries()) { for (const { custodyColumns } of peersSyncMeta.values()) { if (custodyColumns.includes(column)) { peersByColumnIndex.set(columnIndex, (peersByColumnIndex.get(columnIndex) ?? 0) + 1); } } } for (let columnIndex = 0; columnIndex < this.custodyConfig.sampledColumns.length; columnIndex++) { const peerCount = peersByColumnIndex.get(columnIndex) ?? 0; syncPeersMetric.set({ columnIndex }, peerCount); } } } /** * Enforces that a report peer action is defined for all BatchErrorCode exhaustively. * If peer should not be downscored, returns null. */ export function shouldReportPeerOnBatchError(code) { switch (code) { // A batch could not be processed after max retry limit. It's likely that all peers // in this chain are sending invalid batches repeatedly so are either malicious or faulty. // We drop the chain and report all peers. // There are some edge cases with forks that could cause this situation, but it's unlikely. case BatchErrorCode.MAX_PROCESSING_ATTEMPTS: return { action: PeerAction.LowToleranceError, reason: "SyncChainMaxProcessingAttempts" }; // TODO: Should peers be reported for MAX_DOWNLOAD_ATTEMPTS? case BatchErrorCode.MAX_DOWNLOAD_ATTEMPTS: case BatchErrorCode.INVALID_COUNT: case BatchErrorCode.WRONG_STATUS: case BatchErrorCode.MAX_EXECUTION_ENGINE_ERROR_ATTEMPTS: return null; } } //# sourceMappingURL=chain.js.map