diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts index d19207abdb61..88d56171c7a3 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksExecutionPayloads.ts @@ -25,7 +25,7 @@ import {IExecutionEngine} from "../../execution/engine/interface.js"; import {BlockError, BlockErrorCode} from "../errors/index.js"; import {IClock} from "../../util/clock.js"; import {BlockProcessOpts} from "../options.js"; -import {ExecutePayloadStatus} from "../../execution/engine/interface.js"; +import {ExecutionPayloadStatus} from "../../execution/engine/interface.js"; import {IEth1ForBlockProduction} from "../../eth1/index.js"; import {Metrics} from "../../metrics/metrics.js"; import {ImportBlockOpts} from "./types.js"; @@ -304,13 +304,13 @@ export async function verifyBlockExecutionPayload( chain.metrics?.engineNotifyNewPayloadResult.inc({result: execResult.status}); switch (execResult.status) { - case ExecutePayloadStatus.VALID: { + case ExecutionPayloadStatus.VALID: { const executionStatus: ExecutionStatus.Valid = ExecutionStatus.Valid; const lvhResponse = {executionStatus, latestValidExecHash: execResult.latestValidHash}; return {executionStatus, lvhResponse, execError: null}; } - case ExecutePayloadStatus.INVALID: { + case ExecutionPayloadStatus.INVALID: { const executionStatus: ExecutionStatus.Invalid = ExecutionStatus.Invalid; const lvhResponse = { executionStatus, @@ -326,15 +326,15 @@ export async function verifyBlockExecutionPayload( } // Accepted and Syncing have the same treatment, as final validation of block is pending - case ExecutePayloadStatus.ACCEPTED: - case ExecutePayloadStatus.SYNCING: { + case ExecutionPayloadStatus.ACCEPTED: + case ExecutionPayloadStatus.SYNCING: { // Check if the entire segment was deemed safe or, this block specifically itself if not in // the safeSlotsToImportOptimistically window of current slot, then we can import else // we need to throw and not import his block if (!isOptimisticallySafe && block.message.slot + opts.safeSlotsToImportOptimistically >= currentSlot) { const execError = new BlockError(block, { code: BlockErrorCode.EXECUTION_ENGINE_ERROR, - execStatus: ExecutePayloadStatus.UNSAFE_OPTIMISTIC_STATUS, + execStatus: ExecutionPayloadStatus.UNSAFE_OPTIMISTIC_STATUS, errorMessage: `not safe to import ${execResult.status} payload within ${opts.safeSlotsToImportOptimistically} of currentSlot`, }); return {executionStatus: null, execError} as VerifyBlockExecutionResponse; @@ -360,9 +360,9 @@ export async function verifyBlockExecutionPayload( // back. But for now, lets assume other mechanisms like unknown parent block of a future // child block will cause it to replay - case ExecutePayloadStatus.INVALID_BLOCK_HASH: - case ExecutePayloadStatus.ELERROR: - case ExecutePayloadStatus.UNAVAILABLE: { + case ExecutionPayloadStatus.INVALID_BLOCK_HASH: + case ExecutionPayloadStatus.ELERROR: + case ExecutionPayloadStatus.UNAVAILABLE: { const execError = new BlockError(block, { code: BlockErrorCode.EXECUTION_ENGINE_ERROR, execStatus: execResult.status, diff --git a/packages/beacon-node/src/chain/errors/blockError.ts b/packages/beacon-node/src/chain/errors/blockError.ts index 7438e662614b..59775f5694fb 100644 --- a/packages/beacon-node/src/chain/errors/blockError.ts +++ b/packages/beacon-node/src/chain/errors/blockError.ts @@ -2,7 +2,7 @@ import {toHexString} from "@chainsafe/ssz"; import {allForks, RootHex, Slot, ValidatorIndex} from "@lodestar/types"; import {LodestarError} from "@lodestar/utils"; import {CachedBeaconStateAllForks} from "@lodestar/state-transition"; -import {ExecutePayloadStatus} from "../../execution/engine/interface.js"; +import {ExecutionPayloadStatus} from "../../execution/engine/interface.js"; import {QueueErrorCode} from "../../util/queue/index.js"; import {GossipActionError} from "./gossipValidation.js"; @@ -66,8 +66,8 @@ export enum BlockErrorCode { } type ExecutionErrorStatus = Exclude< - ExecutePayloadStatus, - ExecutePayloadStatus.VALID | ExecutePayloadStatus.ACCEPTED | ExecutePayloadStatus.SYNCING + ExecutionPayloadStatus, + ExecutionPayloadStatus.VALID | ExecutionPayloadStatus.ACCEPTED | ExecutionPayloadStatus.SYNCING >; export type BlockErrorType = diff --git a/packages/beacon-node/src/execution/engine/http.ts b/packages/beacon-node/src/execution/engine/http.ts index ea89506c2fc4..a2fc6ebb334d 100644 --- a/packages/beacon-node/src/execution/engine/http.ts +++ b/packages/beacon-node/src/execution/engine/http.ts @@ -1,16 +1,19 @@ import {Root, RootHex, allForks, Wei} from "@lodestar/types"; import {SLOTS_PER_EPOCH, ForkName, ForkSeq} from "@lodestar/params"; import {Logger} from "@lodestar/logger"; -import {isErrorAborted} from "@lodestar/utils"; -import {ErrorJsonRpcResponse, HttpRpcError} from "../../eth1/provider/jsonRpcHttpClient.js"; -import {IJsonRpcHttpClient, ReqOpts} from "../../eth1/provider/jsonRpcHttpClient.js"; +import { + ErrorJsonRpcResponse, + HttpRpcError, + IJsonRpcHttpClient, + ReqOpts, +} from "../../eth1/provider/jsonRpcHttpClient.js"; import {Metrics} from "../../metrics/index.js"; -import {JobItemQueue, isQueueErrorAborted} from "../../util/queue/index.js"; +import {JobItemQueue} from "../../util/queue/index.js"; import {EPOCHS_PER_BATCH} from "../../sync/constants.js"; import {numToQuantity} from "../../eth1/provider/utils.js"; import {IJson, RpcPayload} from "../../eth1/interface.js"; import { - ExecutePayloadStatus, + ExecutionPayloadStatus, ExecutePayloadResponse, IExecutionEngine, PayloadId, @@ -131,9 +134,12 @@ export class ExecutionEngineHttp implements IExecutionEngine { this.updateEngineState(ExecutionEngineState.ONLINE); return res; } catch (err) { - if (!isErrorAborted(err)) { - this.updateEngineState(getExecutionEngineState({payloadError: err})); - } + this.updateEngineState(getExecutionEngineState({payloadError: err, oldState: this.state})); + + /* + * TODO: For some error cases as abort, we may not want to escalate the error to the caller + * But for now the higher level code handles such cases so we can just rethrow the error + */ throw err; } } @@ -207,39 +213,34 @@ export class ExecutionEngineHttp implements IExecutionEngine { const {status, latestValidHash, validationError} = await ( this.rpcFetchQueue.push(engineRequest) as Promise - ) - // If there are errors by EL like connection refused, internal error, they need to be - // treated separate from being INVALID. For now, just pass the error upstream. - .catch((e: Error): EngineApiRpcReturnTypes[typeof method] => { - if (!isErrorAborted(e) && !isQueueErrorAborted(e)) { - this.updateEngineState(getExecutionEngineState({payloadError: e})); - } - if (e instanceof HttpRpcError || e instanceof ErrorJsonRpcResponse) { - return {status: ExecutePayloadStatus.ELERROR, latestValidHash: null, validationError: e.message}; - } else { - return {status: ExecutePayloadStatus.UNAVAILABLE, latestValidHash: null, validationError: e.message}; - } - }); - this.updateEngineState(getExecutionEngineState({payloadStatus: status})); + ).catch((e: Error) => { + if (e instanceof HttpRpcError || e instanceof ErrorJsonRpcResponse) { + return {status: ExecutionPayloadStatus.ELERROR, latestValidHash: null, validationError: e.message}; + } else { + return {status: ExecutionPayloadStatus.UNAVAILABLE, latestValidHash: null, validationError: e.message}; + } + }); + + this.updateEngineState(getExecutionEngineState({payloadStatus: status, oldState: this.state})); switch (status) { - case ExecutePayloadStatus.VALID: + case ExecutionPayloadStatus.VALID: return {status, latestValidHash: latestValidHash ?? "0x0", validationError: null}; - case ExecutePayloadStatus.INVALID: + case ExecutionPayloadStatus.INVALID: // As per latest specs if latestValidHash can be null and it would mean only // invalidate this block return {status, latestValidHash, validationError}; - case ExecutePayloadStatus.SYNCING: - case ExecutePayloadStatus.ACCEPTED: + case ExecutionPayloadStatus.SYNCING: + case ExecutionPayloadStatus.ACCEPTED: return {status, latestValidHash: null, validationError: null}; - case ExecutePayloadStatus.INVALID_BLOCK_HASH: + case ExecutionPayloadStatus.INVALID_BLOCK_HASH: return {status, latestValidHash: null, validationError: validationError ?? "Malformed block"}; - case ExecutePayloadStatus.UNAVAILABLE: - case ExecutePayloadStatus.ELERROR: + case ExecutionPayloadStatus.UNAVAILABLE: + case ExecutionPayloadStatus.ELERROR: return { status, latestValidHash: null, @@ -248,7 +249,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { default: return { - status: ExecutePayloadStatus.ELERROR, + status: ExecutionPayloadStatus.ELERROR, latestValidHash: null, validationError: `Invalid EL status on executePayload: ${status}`, }; @@ -312,25 +313,15 @@ export class ExecutionEngineHttp implements IExecutionEngine { methodOpts: fcUReqOpts, }) as Promise; - const response = await request - // If there are errors by EL like connection refused, internal error, they need to be - // treated separate from being INVALID. For now, just pass the error upstream. - .catch((e: Error): EngineApiRpcReturnTypes[typeof method] => { - if (!isErrorAborted(e) && !isQueueErrorAborted(e)) { - this.updateEngineState(getExecutionEngineState({payloadError: e})); - } - throw e; - }); - const { payloadStatus: {status, latestValidHash: _latestValidHash, validationError}, payloadId, - } = response; + } = await request; - this.updateEngineState(getExecutionEngineState({payloadStatus: status})); + this.updateEngineState(getExecutionEngineState({payloadStatus: status, oldState: this.state})); switch (status) { - case ExecutePayloadStatus.VALID: + case ExecutionPayloadStatus.VALID: // if payloadAttributes are provided, a valid payloadId is expected if (payloadAttributesRpc) { if (!payloadId || payloadId === "0x") { @@ -342,7 +333,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { } return payloadId !== "0x" ? payloadId : null; - case ExecutePayloadStatus.SYNCING: + case ExecutionPayloadStatus.SYNCING: // Throw error on syncing if requested to produce a block, else silently ignore if (payloadAttributes) { throw Error("Execution Layer Syncing"); @@ -350,7 +341,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { return null; } - case ExecutePayloadStatus.INVALID: + case ExecutionPayloadStatus.INVALID: throw Error( `Invalid ${payloadAttributes ? "prepare payload" : "forkchoice request"}, validationError=${ validationError ?? "" @@ -430,29 +421,21 @@ export class ExecutionEngineHttp implements IExecutionEngine { if (oldState === newState) return; - // The ONLINE is initial state and can reached from offline or auth failed error - if ( - newState === ExecutionEngineState.ONLINE && - !(oldState === ExecutionEngineState.OFFLINE || oldState === ExecutionEngineState.AUTH_FAILED) - ) { - return; - } - switch (newState) { case ExecutionEngineState.ONLINE: - this.logger.info("Execution client became online"); + this.logger.info("Execution client became online", {oldState, newState}); break; case ExecutionEngineState.OFFLINE: - this.logger.error("Execution client went offline"); + this.logger.error("Execution client went offline", {oldState, newState}); break; case ExecutionEngineState.SYNCED: - this.logger.info("Execution client is synced"); + this.logger.info("Execution client is synced", {oldState, newState}); break; case ExecutionEngineState.SYNCING: - this.logger.warn("Execution client is syncing"); + this.logger.warn("Execution client is syncing", {oldState, newState}); break; case ExecutionEngineState.AUTH_FAILED: - this.logger.error("Execution client authentication failed"); + this.logger.error("Execution client authentication failed", {oldState, newState}); break; } diff --git a/packages/beacon-node/src/execution/engine/interface.ts b/packages/beacon-node/src/execution/engine/interface.ts index 92eff776cacb..f2ce00e43a45 100644 --- a/packages/beacon-node/src/execution/engine/interface.ts +++ b/packages/beacon-node/src/execution/engine/interface.ts @@ -8,7 +8,7 @@ import {ExecutionPayloadBody} from "./types.js"; export {PayloadIdCache, PayloadId, WithdrawalV1}; -export enum ExecutePayloadStatus { +export enum ExecutionPayloadStatus { /** given payload is valid */ VALID = "VALID", /** given payload is invalid */ @@ -39,19 +39,26 @@ export enum ExecutionEngineState { } export type ExecutePayloadResponse = - | {status: ExecutePayloadStatus.SYNCING | ExecutePayloadStatus.ACCEPTED; latestValidHash: null; validationError: null} - | {status: ExecutePayloadStatus.VALID; latestValidHash: RootHex; validationError: null} - | {status: ExecutePayloadStatus.INVALID; latestValidHash: RootHex | null; validationError: string | null} | { - status: ExecutePayloadStatus.INVALID_BLOCK_HASH | ExecutePayloadStatus.ELERROR | ExecutePayloadStatus.UNAVAILABLE; + status: ExecutionPayloadStatus.SYNCING | ExecutionPayloadStatus.ACCEPTED; + latestValidHash: null; + validationError: null; + } + | {status: ExecutionPayloadStatus.VALID; latestValidHash: RootHex; validationError: null} + | {status: ExecutionPayloadStatus.INVALID; latestValidHash: RootHex | null; validationError: string | null} + | { + status: + | ExecutionPayloadStatus.INVALID_BLOCK_HASH + | ExecutionPayloadStatus.ELERROR + | ExecutionPayloadStatus.UNAVAILABLE; latestValidHash: null; validationError: string; }; export type ForkChoiceUpdateStatus = - | ExecutePayloadStatus.VALID - | ExecutePayloadStatus.INVALID - | ExecutePayloadStatus.SYNCING; + | ExecutionPayloadStatus.VALID + | ExecutionPayloadStatus.INVALID + | ExecutionPayloadStatus.SYNCING; export type PayloadAttributes = { timestamp: number; diff --git a/packages/beacon-node/src/execution/engine/mock.ts b/packages/beacon-node/src/execution/engine/mock.ts index eea528d1a50a..d2bc3303c042 100644 --- a/packages/beacon-node/src/execution/engine/mock.ts +++ b/packages/beacon-node/src/execution/engine/mock.ts @@ -24,7 +24,7 @@ import { BlobsBundleRpc, ExecutionPayloadBodyRpc, } from "./types.js"; -import {ExecutePayloadStatus, PayloadIdCache} from "./interface.js"; +import {ExecutionPayloadStatus, PayloadIdCache} from "./interface.js"; import {JsonRpcBackend} from "./utils.js"; const INTEROP_GAS_LIMIT = 30e6; @@ -171,7 +171,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { if (!this.validBlocks.has(parentHash)) { // if requisite data for the payload's acceptance or validation is missing // return {status: SYNCING, latestValidHash: null, validationError: null} - return {status: ExecutePayloadStatus.SYNCING, latestValidHash: null, validationError: null}; + return {status: ExecutionPayloadStatus.SYNCING, latestValidHash: null, validationError: null}; } // 4. Client software MAY NOT validate the payload if the payload doesn't belong to the canonical chain. @@ -190,7 +190,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { // IF the payload has been fully validated while processing the call // RETURN payload status from the Payload validation process // If validation succeeds, the response MUST contain {status: VALID, latestValidHash: payload.blockHash} - return {status: ExecutePayloadStatus.VALID, latestValidHash: blockHash, validationError: null}; + return {status: ExecutionPayloadStatus.VALID, latestValidHash: blockHash, validationError: null}; } /** @@ -246,7 +246,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { // // > TODO: Implement return { - payloadStatus: {status: ExecutePayloadStatus.SYNCING, latestValidHash: null, validationError: null}, + payloadStatus: {status: ExecutionPayloadStatus.SYNCING, latestValidHash: null, validationError: null}, payloadId: null, }; } @@ -337,7 +337,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { // IF the payload is deemed VALID and the build process has begun // {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash, validationError: null}, payloadId: buildProcessId} return { - payloadStatus: {status: ExecutePayloadStatus.VALID, latestValidHash: null, validationError: null}, + payloadStatus: {status: ExecutionPayloadStatus.VALID, latestValidHash: null, validationError: null}, payloadId: String(payloadId as number), }; } @@ -347,7 +347,7 @@ export class ExecutionEngineMockBackend implements JsonRpcBackend { // IF the payload is deemed VALID and a build process hasn't been started // {payloadStatus: {status: VALID, latestValidHash: forkchoiceState.headBlockHash, validationError: null}, payloadId: null} return { - payloadStatus: {status: ExecutePayloadStatus.VALID, latestValidHash: null, validationError: null}, + payloadStatus: {status: ExecutionPayloadStatus.VALID, latestValidHash: null, validationError: null}, payloadId: null, }; } diff --git a/packages/beacon-node/src/execution/engine/types.ts b/packages/beacon-node/src/execution/engine/types.ts index 3f7254f12976..d400df03a585 100644 --- a/packages/beacon-node/src/execution/engine/types.ts +++ b/packages/beacon-node/src/execution/engine/types.ts @@ -16,7 +16,7 @@ import { QUANTITY, quantityToBigint, } from "../../eth1/provider/utils.js"; -import {ExecutePayloadStatus, BlobsBundle, PayloadAttributes, VersionedHashes} from "./interface.js"; +import {ExecutionPayloadStatus, BlobsBundle, PayloadAttributes, VersionedHashes} from "./interface.js"; import {WithdrawalV1} from "./payloadIdCache.js"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -65,7 +65,7 @@ export type EngineApiRpcParamTypes = { }; export type PayloadStatus = { - status: ExecutePayloadStatus; + status: ExecutionPayloadStatus; latestValidHash: DATA | null; validationError: string | null; }; diff --git a/packages/beacon-node/src/execution/engine/utils.ts b/packages/beacon-node/src/execution/engine/utils.ts index 3218d403baa6..5ce2336f6db3 100644 --- a/packages/beacon-node/src/execution/engine/utils.ts +++ b/packages/beacon-node/src/execution/engine/utils.ts @@ -1,7 +1,9 @@ import {isFetchError} from "@lodestar/api"; +import {isErrorAborted} from "@lodestar/utils"; import {IJson, RpcPayload} from "../../eth1/interface.js"; -import {IJsonRpcHttpClient} from "../../eth1/provider/jsonRpcHttpClient.js"; -import {ExecutePayloadStatus, ExecutionEngineState} from "./interface.js"; +import {IJsonRpcHttpClient, ErrorJsonRpcResponse, HttpRpcError} from "../../eth1/provider/jsonRpcHttpClient.js"; +import {isQueueErrorAborted} from "../../util/queue/errors.js"; +import {ExecutionPayloadStatus, ExecutionEngineState} from "./interface.js"; export type JsonRpcBackend = { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -30,40 +32,77 @@ export class ExecutionEngineMockJsonRpcClient implements IJsonRpcHttpClient { } } -const fatalErrorCodes = ["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN"]; -const connectionErrorCodes = ["ECONNRESET", "ECONNABORTED"]; +export const HTTP_FATAL_ERROR_CODES = ["ECONNREFUSED", "ENOTFOUND", "EAI_AGAIN"]; +export const HTTP_CONNECTION_ERROR_CODES = ["ECONNRESET", "ECONNABORTED"]; -export function getExecutionEngineState({ - payloadError, - payloadStatus, -}: - | {payloadStatus: ExecutePayloadStatus; payloadError?: never} - | {payloadStatus?: never; payloadError: unknown}): ExecutionEngineState { +function getExecutionEngineStateForPayloadStatus(payloadStatus: ExecutionPayloadStatus): ExecutionEngineState { switch (payloadStatus) { - case ExecutePayloadStatus.ACCEPTED: - case ExecutePayloadStatus.VALID: - case ExecutePayloadStatus.UNSAFE_OPTIMISTIC_STATUS: + case ExecutionPayloadStatus.ACCEPTED: + case ExecutionPayloadStatus.VALID: + case ExecutionPayloadStatus.UNSAFE_OPTIMISTIC_STATUS: return ExecutionEngineState.SYNCED; - case ExecutePayloadStatus.ELERROR: - case ExecutePayloadStatus.INVALID: - case ExecutePayloadStatus.SYNCING: - case ExecutePayloadStatus.INVALID_BLOCK_HASH: + case ExecutionPayloadStatus.ELERROR: + case ExecutionPayloadStatus.INVALID: + case ExecutionPayloadStatus.SYNCING: + case ExecutionPayloadStatus.INVALID_BLOCK_HASH: return ExecutionEngineState.SYNCING; - case ExecutePayloadStatus.UNAVAILABLE: + case ExecutionPayloadStatus.UNAVAILABLE: return ExecutionEngineState.OFFLINE; + + default: + // In case we can't determine the state, we assume it stays in old state + // This assumption is better than considering offline, because the offline state may trigger some notifications + return ExecutionEngineState.ONLINE; + } +} + +function getExecutionEngineStateForPayloadError( + payloadError: unknown, + oldState: ExecutionEngineState +): ExecutionEngineState { + if (isErrorAborted(payloadError) || isQueueErrorAborted(payloadError)) { + return oldState; + } + + // Originally this case was handled with {status: ExecutePayloadStatus.ELERROR} + if (payloadError instanceof HttpRpcError || payloadError instanceof ErrorJsonRpcResponse) { + return ExecutionEngineState.SYNCING; } - if (payloadError && isFetchError(payloadError) && fatalErrorCodes.includes(payloadError.code)) { + if (payloadError && isFetchError(payloadError) && HTTP_FATAL_ERROR_CODES.includes(payloadError.code)) { return ExecutionEngineState.OFFLINE; } - if (payloadError && isFetchError(payloadError) && connectionErrorCodes.includes(payloadError.code)) { + if (payloadError && isFetchError(payloadError) && HTTP_CONNECTION_ERROR_CODES.includes(payloadError.code)) { return ExecutionEngineState.AUTH_FAILED; } - // In case we can't determine the state, we assume it's online - // This assumption is better than considering offline, because the offline state may trigger some notifications - return ExecutionEngineState.ONLINE; + return oldState; +} + +export function getExecutionEngineState({ + payloadError, + payloadStatus, + oldState, +}: + | {payloadStatus: S; payloadError?: never; oldState: ExecutionEngineState} + | {payloadStatus?: never; payloadError: E; oldState: ExecutionEngineState}): ExecutionEngineState { + const newState = + payloadStatus === undefined + ? getExecutionEngineStateForPayloadError(payloadError, oldState) + : getExecutionEngineStateForPayloadStatus(payloadStatus); + + if (newState === oldState) return oldState; + + // The ONLINE is initial state and can reached from offline or auth failed error + if ( + newState === ExecutionEngineState.ONLINE && + !(oldState === ExecutionEngineState.OFFLINE || oldState === ExecutionEngineState.AUTH_FAILED) + ) { + return oldState; + } + + return newState; } diff --git a/packages/beacon-node/test/sim/merge-interop.test.ts b/packages/beacon-node/test/sim/merge-interop.test.ts index 4a57b9308430..3f9d6f934bd8 100644 --- a/packages/beacon-node/test/sim/merge-interop.test.ts +++ b/packages/beacon-node/test/sim/merge-interop.test.ts @@ -10,7 +10,7 @@ import {routes} from "@lodestar/api"; import {Epoch} from "@lodestar/types"; import {ValidatorProposerConfig} from "@lodestar/validator"; -import {ExecutePayloadStatus, PayloadAttributes} from "../../src/execution/engine/interface.js"; +import {ExecutionPayloadStatus, PayloadAttributes} from "../../src/execution/engine/interface.js"; import {initializeExecutionEngine} from "../../src/execution/index.js"; import {ClockEvent} from "../../src/util/clock.js"; import {testLogger, TestLoggerOpts} from "../utils/logger.js"; @@ -159,7 +159,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { **/ const payloadResult = await executionEngine.notifyNewPayload(ForkName.bellatrix, payload); - if (payloadResult.status !== ExecutePayloadStatus.VALID) { + if (payloadResult.status !== ExecutionPayloadStatus.VALID) { throw Error("getPayload returned payload that notifyNewPayload deems invalid"); } diff --git a/packages/beacon-node/test/sim/withdrawal-interop.test.ts b/packages/beacon-node/test/sim/withdrawal-interop.test.ts index 29bf5a4e315a..4243d9175f14 100644 --- a/packages/beacon-node/test/sim/withdrawal-interop.test.ts +++ b/packages/beacon-node/test/sim/withdrawal-interop.test.ts @@ -9,7 +9,7 @@ import {computeStartSlotAtEpoch} from "@lodestar/state-transition"; import {Epoch, capella, Slot} from "@lodestar/types"; import {ValidatorProposerConfig} from "@lodestar/validator"; -import {ExecutePayloadStatus, PayloadAttributes} from "../../src/execution/engine/interface.js"; +import {ExecutionPayloadStatus, PayloadAttributes} from "../../src/execution/engine/interface.js"; import {initializeExecutionEngine} from "../../src/execution/index.js"; import {ClockEvent} from "../../src/util/clock.js"; import {testLogger, TestLoggerOpts} from "../utils/logger.js"; @@ -171,7 +171,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { // 3. Execute the payload const payloadResult = await executionEngine.notifyNewPayload(ForkName.capella, payload); - if (payloadResult.status !== ExecutePayloadStatus.VALID) { + if (payloadResult.status !== ExecutionPayloadStatus.VALID) { throw Error("getPayload returned payload that notifyNewPayload deems invalid"); } diff --git a/packages/beacon-node/test/spec/presets/fork_choice.ts b/packages/beacon-node/test/spec/presets/fork_choice.ts index c98d4419499a..53aa57479bad 100644 --- a/packages/beacon-node/test/spec/presets/fork_choice.ts +++ b/packages/beacon-node/test/spec/presets/fork_choice.ts @@ -15,7 +15,7 @@ import {getConfig} from "../../utils/config.js"; import {TestRunnerFn} from "../utils/types.js"; import {Eth1ForBlockProductionDisabled} from "../../../src/eth1/index.js"; import {getExecutionEngineFromBackend} from "../../../src/execution/index.js"; -import {ExecutePayloadStatus} from "../../../src/execution/engine/interface.js"; +import {ExecutionPayloadStatus} from "../../../src/execution/engine/interface.js"; import {ExecutionEngineMockBackend} from "../../../src/execution/engine/mock.js"; import {defaultChainOptions} from "../../../src/chain/options.js"; import {getStubbedBeaconDb} from "../../utils/mocks/db.js"; @@ -201,7 +201,7 @@ export const forkChoiceTest = // Optional step for optimistic sync tests. else if (isOnPayloadInfoStep(step)) { logger.debug(`Step ${i}/${stepsLen} payload_status`, {blockHash: step.block_hash}); - const status = ExecutePayloadStatus[step.payload_status.status]; + const status = ExecutionPayloadStatus[step.payload_status.status]; if (status === undefined) { throw Error(`Unknown payload_status.status: ${step.payload_status.status}`); } diff --git a/packages/beacon-node/test/unit/execution/engine/utils.test.ts b/packages/beacon-node/test/unit/execution/engine/utils.test.ts new file mode 100644 index 000000000000..997ec8726bd7 --- /dev/null +++ b/packages/beacon-node/test/unit/execution/engine/utils.test.ts @@ -0,0 +1,192 @@ +import {expect} from "chai"; +import {ErrorAborted} from "@lodestar/utils"; +import {FetchError} from "@lodestar/api"; +import {ExecutionPayloadStatus, ExecutionEngineState} from "../../../../src/execution/index.js"; +import { + HTTP_CONNECTION_ERROR_CODES, + HTTP_FATAL_ERROR_CODES, + getExecutionEngineState, +} from "../../../../src/execution/engine/utils.js"; +import {QueueError, QueueErrorCode} from "../../../../src/util/queue/errors.js"; +import {ErrorJsonRpcResponse, HttpRpcError} from "../../../../src/eth1/provider/jsonRpcHttpClient.js"; + +describe("execution / engine / utils", () => { + describe("getExecutionEngineState", () => { + const testCasesPayload: Record< + ExecutionPayloadStatus, + [oldState: ExecutionEngineState, newState: ExecutionEngineState][] + > = { + [ExecutionPayloadStatus.ACCEPTED]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCED], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCED], + ], + [ExecutionPayloadStatus.VALID]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCED], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCED], + ], + [ExecutionPayloadStatus.UNSAFE_OPTIMISTIC_STATUS]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCED], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCED], + ], + [ExecutionPayloadStatus.ELERROR]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + [ExecutionPayloadStatus.INVALID]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + [ExecutionPayloadStatus.SYNCING]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + [ExecutionPayloadStatus.INVALID_BLOCK_HASH]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + [ExecutionPayloadStatus.UNAVAILABLE]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.SYNCED, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.SYNCING, ExecutionEngineState.OFFLINE], + ], + ["unknown" as ExecutionPayloadStatus]: [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.ONLINE], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.ONLINE], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.ONLINE], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + }; + + type ErrorTestCase = [string, Error, [oldState: ExecutionEngineState, newState: ExecutionEngineState][]]; + const testCasesError: ErrorTestCase[] = [ + [ + "abort error", + new ErrorAborted(), + [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.ONLINE], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.AUTH_FAILED], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + ], + [ + "queue aborted error", + new QueueError({code: QueueErrorCode.QUEUE_ABORTED}), + [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.ONLINE], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.AUTH_FAILED], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + ], + [ + "rpc error", + new HttpRpcError(12, "error"), + [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + ], + [ + "rpc response error", + new ErrorJsonRpcResponse({jsonrpc: "2.0", id: 123, error: {code: 123, message: "error"}}, "error"), + [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCING], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + ], + ...HTTP_FATAL_ERROR_CODES.map((code) => { + const error = new FetchError("http://localhost:1234", new TypeError("error")); + error.code = code; + + return [ + `http error with code '${code}'`, + error, + [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.SYNCED, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.SYNCING, ExecutionEngineState.OFFLINE], + ], + ] as ErrorTestCase; + }), + ...HTTP_CONNECTION_ERROR_CODES.map((code) => { + const error = new FetchError("http://localhost:1234", new TypeError("error")); + error.code = code; + + return [ + `http error with code '${code}'`, + error, + [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.AUTH_FAILED], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.AUTH_FAILED], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.AUTH_FAILED], + [ExecutionEngineState.SYNCED, ExecutionEngineState.AUTH_FAILED], + [ExecutionEngineState.SYNCING, ExecutionEngineState.AUTH_FAILED], + ], + ] as ErrorTestCase; + }), + [ + "unknown error", + new Error("unknown error"), + [ + [ExecutionEngineState.ONLINE, ExecutionEngineState.ONLINE], + [ExecutionEngineState.AUTH_FAILED, ExecutionEngineState.AUTH_FAILED], + [ExecutionEngineState.OFFLINE, ExecutionEngineState.OFFLINE], + [ExecutionEngineState.SYNCED, ExecutionEngineState.SYNCED], + [ExecutionEngineState.SYNCING, ExecutionEngineState.SYNCING], + ], + ], + ]; + + for (const payloadStatus of Object.keys(testCasesPayload) as ExecutionPayloadStatus[]) { + for (const [oldState, newState] of testCasesPayload[payloadStatus]) { + it(`should transition from "${oldState}" to "${newState}" on payload status "${payloadStatus}"`, () => { + expect(getExecutionEngineState({payloadStatus, oldState})).to.equal(newState); + }); + } + } + + for (const testCase of testCasesError) { + const [message, payloadError, errorCases] = testCase; + for (const [oldState, newState] of errorCases) { + it(`should transition from "${oldState}" to "${newState}" on error "${message}"`, () => { + expect(getExecutionEngineState({payloadError, oldState})).to.equal(newState); + }); + } + } + }); +});