@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
194 lines • 10.6 kB
JavaScript
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