UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

194 lines • 10.6 kB
import { ExecutionStatus, } from "@lodestar/fork-choice"; import { ForkSeq } from "@lodestar/params"; import { isExecutionBlockBodyType, isStatePostBellatrix } from "@lodestar/state-transition"; import { ErrorAborted, toRootHex } from "@lodestar/utils"; import { ExecutionPayloadStatus } from "../../execution/engine/interface.js"; import { BlockError, BlockErrorCode } from "../errors/index.js"; import { isBlockInputBlobs, isBlockInputColumns, isBlockInputNoData } from "./blockInput/blockInput.js"; /** * Verifies 1 or more execution payloads from a linear sequence of blocks. * * Since the EL client must be aware of each parent, all payloads must be submitted in sequence. */ export async function verifyBlocksExecutionPayload(chain, parentBlock, blockInputs, preState0, signal, opts) { const executionStatuses = []; const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000); const lastBlock = blockInputs.at(-1); // Error in the same way as verifyBlocksSanityChecks if empty blocks if (!lastBlock) { throw Error("Empty partiallyVerifiedBlocks"); } // For a block with SYNCING status (called optimistic block), it's okay to import with // SYNCING status as EL could switch into syncing // // 1. On initial startup/restart // 2. When some reorg might have occurred and EL doesn't has a parent root // (observed on devnets) // 3. Because of some unavailable (and potentially invalid) root but there is no way // of knowing if this is invalid/unavailable. For unavailable block, some proposer // will (sooner or later) build on the available parent head which will // eventually win in fork-choice as other validators vote on VALID blocks. // // Once EL catches up again and respond VALID, the fork choice will be updated which // will either validate or prune invalid blocks // // We need to track and keep updating if its safe to optimistically import these blocks. // // When to import such blocks: // From: https://github.com/ethereum/consensus-specs/pull/2844 for (let blockIndex = 0; blockIndex < blockInputs.length; blockIndex++) { const blockInput = blockInputs[blockIndex]; // If blocks are invalid in consensus the main promise could resolve before this loop ends. // In that case stop sending blocks to execution engine if (signal.aborted) { throw new ErrorAborted("verifyBlockExecutionPayloads"); } const verifyResponse = await verifyBlockExecutionPayload(chain, blockInput, preState0); // If execError has happened, then we need to extract the segmentExecStatus and return if (verifyResponse.execError !== null) { return getSegmentErrorResponse({ verifyResponse, blockIndex }, parentBlock, blockInputs); } // If we are here then its because executionStatus is one of BlockExecutionStatus const { executionStatus } = verifyResponse; executionStatuses.push(executionStatus); } const executionTime = Date.now(); if (blockInputs.length === 1 && opts.seenTimestampSec !== undefined && executionStatuses[0] === ExecutionStatus.Valid) { const recvToValidation = executionTime / 1000 - opts.seenTimestampSec; const validationTime = recvToValidation - recvToValLatency; chain.metrics?.gossipBlock.executionPayload.recvToValidation.observe(recvToValidation); chain.metrics?.gossipBlock.executionPayload.validationTime.observe(validationTime); chain.logger.debug("Verified execution payload", { slot: blockInputs[0].slot, recvToValLatency, recvToValidation, validationTime, }); } return { execAborted: null, executionStatuses, executionTime, }; } /** * Verifies a single block execution payload by sending it to the EL client (via HTTP). */ export async function verifyBlockExecutionPayload(chain, blockInput, preState0) { const block = blockInput.getBlock(); // Gloas block doesn't have execution payload. Return Syncing as a placeholder; the actual // status for gloas PENDING/EMPTY is derived from parent's chain in importBlock. if (isBlockInputNoData(blockInput)) { return { executionStatus: ExecutionStatus.Syncing, lvhResponse: undefined, execError: null }; } /** Not null if execution is enabled */ const executionPayloadEnabled = isStatePostBellatrix(preState0) && preState0.isExecutionStateType && isExecutionBlockBodyType(block.message.body) && preState0.isExecutionEnabled(block.message) ? block.message.body.executionPayload : null; if (!executionPayloadEnabled) { // Pre-merge block, no execution payload to verify return { executionStatus: ExecutionStatus.PreMerge, lvhResponse: undefined, execError: null }; } // TODO: Handle better notifyNewPayload() returning error is syncing const fork = blockInput.forkName; const versionedHashes = isBlockInputBlobs(blockInput) || isBlockInputColumns(blockInput) ? blockInput.getVersionedHashes() : undefined; const parentBlockRoot = ForkSeq[fork] >= ForkSeq.deneb ? block.message.parentRoot : undefined; const executionRequests = ForkSeq[fork] >= ForkSeq.electra ? block.message.body.executionRequests : undefined; const logCtx = { slot: blockInput.slot, executionBlock: executionPayloadEnabled.blockNumber }; chain.logger.debug("Call engine api newPayload", logCtx); const execResult = await chain.executionEngine.notifyNewPayload(fork, executionPayloadEnabled, versionedHashes, parentBlockRoot, executionRequests); chain.logger.debug("Receive engine api newPayload result", { ...logCtx, status: execResult.status }); chain.metrics?.engineNotifyNewPayloadResult.inc({ result: execResult.status }); switch (execResult.status) { case ExecutionPayloadStatus.VALID: { const executionStatus = ExecutionStatus.Valid; const lvhResponse = { executionStatus, latestValidExecHash: execResult.latestValidHash }; return { executionStatus, lvhResponse, execError: null }; } case ExecutionPayloadStatus.INVALID: { const executionStatus = ExecutionStatus.Invalid; const lvhResponse = { executionStatus, latestValidExecHash: execResult.latestValidHash, invalidateFromParentBlockRoot: blockInput.parentRootHex, invalidateFromParentBlockHash: toRootHex(executionPayloadEnabled.parentHash), }; const execError = new BlockError(block, { code: BlockErrorCode.EXECUTION_ENGINE_ERROR, execStatus: execResult.status, errorMessage: execResult.validationError ?? "", }); return { executionStatus, lvhResponse, execError }; } // Accepted and Syncing have the same treatment, as final validation of block is pending // Post-merge, we're always safe to optimistically import case ExecutionPayloadStatus.ACCEPTED: case ExecutionPayloadStatus.SYNCING: return { executionStatus: ExecutionStatus.Syncing, execError: null }; // If the block has is not valid, or it referenced an invalid terminal block then the // block is invalid, however it has no bearing on any forkChoice cleanup // // There can be other reasons for which EL failed some of the observed ones are // 1. Connection refused / can't connect to EL port // 2. EL Internal Error // 3. Geth sometimes gives invalid merkle root error which means invalid // but expects it to be handled in CL as of now. But we should log as warning // and give it as optimistic treatment and expect any other non-geth CL<>EL // combination to reject the invalid block and propose a block. // On kintsugi devnet, this has been observed to cause contiguous proposal failures // as the network is geth dominated, till a non geth node proposes and moves network // forward // For network/unreachable errors, an optimization can be added to replay these blocks // back. But for now, lets assume other mechanisms like unknown parent block of a future // child block will cause it to replay case ExecutionPayloadStatus.INVALID_BLOCK_HASH: case ExecutionPayloadStatus.ELERROR: case ExecutionPayloadStatus.UNAVAILABLE: { const execError = new BlockError(block, { code: BlockErrorCode.EXECUTION_ENGINE_ERROR, execStatus: execResult.status, errorMessage: execResult.validationError, }); return { executionStatus: null, execError }; } } } function getSegmentErrorResponse({ verifyResponse, blockIndex }, parentBlock, blocks) { const { executionStatus, lvhResponse, execError } = verifyResponse; let invalidSegmentLVH = undefined; if (executionStatus === ExecutionStatus.Invalid && lvhResponse !== undefined && lvhResponse.latestValidExecHash !== null) { let lvhFound = false; for (let mayBeLVHIndex = blockIndex - 1; mayBeLVHIndex >= 0; mayBeLVHIndex--) { const block = blocks[mayBeLVHIndex].getBlock(); if (toRootHex(block.message.body.executionPayload.blockHash) === lvhResponse.latestValidExecHash) { lvhFound = true; break; } } // If there is no valid in the segment then we have to propagate invalid response // in forkchoice as well if // - if the parentBlock is also not the lvh // - and parentBlock is not pre merge if (!lvhFound && parentBlock.executionStatus !== ExecutionStatus.PreMerge && parentBlock.executionPayloadBlockHash !== lvhResponse.latestValidExecHash) { invalidSegmentLVH = { executionStatus: ExecutionStatus.Invalid, latestValidExecHash: lvhResponse.latestValidExecHash, invalidateFromParentBlockRoot: parentBlock.blockRoot, invalidateFromParentBlockHash: parentBlock.executionPayloadBlockHash, }; } } const execAborted = { blockIndex, execError }; return { execAborted, invalidSegmentLVH }; } //# sourceMappingURL=verifyBlocksExecutionPayloads.js.map