@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
206 lines • 10.5 kB
JavaScript
import { routes } from "@lodestar/api";
import { ExecutionStatus, getSafeExecutionBlockHash } from "@lodestar/fork-choice";
import { isStatePostGloas } from "@lodestar/state-transition";
import { isErrorAborted } from "@lodestar/utils";
import { ZERO_HASH_HEX } from "../../constants/index.js";
import { ExecutionPayloadStatus } from "../../execution/index.js";
import { isQueueErrorAborted } from "../../util/queue/index.js";
import { RegenCaller } from "../regen/interface.js";
import { verifyExecutionPayloadEnvelope, verifyExecutionPayloadEnvelopeSignature, } from "./verifyExecutionPayloadEnvelope.js";
import { verifyPayloadsDataAvailability } from "./verifyPayloadsDataAvailability.js";
const EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS = 64;
export { PayloadErrorCode };
var PayloadErrorCode;
(function (PayloadErrorCode) {
PayloadErrorCode["EXECUTION_ENGINE_INVALID"] = "PAYLOAD_ERROR_EXECUTION_ENGINE_INVALID";
PayloadErrorCode["EXECUTION_ENGINE_ERROR"] = "PAYLOAD_ERROR_EXECUTION_ENGINE_ERROR";
PayloadErrorCode["BLOCK_NOT_IN_FORK_CHOICE"] = "PAYLOAD_ERROR_BLOCK_NOT_IN_FORK_CHOICE";
PayloadErrorCode["MISS_BLOCK_STATE"] = "PAYLOAD_ERROR_MISS_BLOCK_STATE";
PayloadErrorCode["ENVELOPE_VERIFICATION_ERROR"] = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR";
PayloadErrorCode["INVALID_SIGNATURE"] = "PAYLOAD_ERROR_INVALID_SIGNATURE";
})(PayloadErrorCode || (PayloadErrorCode = {}));
export class PayloadError extends Error {
type;
constructor(type, message) {
super(message ?? type.code);
this.type = type;
}
}
function toForkChoiceExecutionStatus(status) {
switch (status) {
case ExecutionPayloadStatus.VALID:
return ExecutionStatus.Valid;
case ExecutionPayloadStatus.SYNCING:
case ExecutionPayloadStatus.ACCEPTED:
return ExecutionStatus.Syncing;
default:
throw new Error(`Unexpected execution payload status for fork choice: ${status}`);
}
}
/**
* Import an execution payload envelope after all data is available.
*
* The envelope is only verified here, no state mutation. State effects from the payload
* are applied on the next block via processParentExecutionPayload.
*
* The DA wait must have run upstream (range sync awaits DA in `verifyBlocksInEpoch` for the
* whole segment; gossip / API path uses the `processExecutionPayload` wrapper below).
*
* Steps:
* 1. Emit `execution_payload_available` event for payload attestation
* 2. Get the ProtoBlock from fork choice
* 3. Regenerate state for envelope verification
* 4. Verify envelope (fields against state, signature, and EL in parallel where possible)
* 5. Persist verified payload envelope to hot DB (waits for write-queue space for backpressure)
* 6. Update fork choice (transitions the block's PENDING variant to FULL)
* 7. Queue notifyForkchoiceUpdate to engine api
* 8. Record metrics for payload envelope and column sources
* 9. Emit `execution_payload` event
*/
export async function importExecutionPayload(payloadInput, dataAvailabilityStatus, opts = {}) {
const signedEnvelope = payloadInput.getPayloadEnvelope();
const envelope = signedEnvelope.message;
const slot = envelope.payload.slotNumber;
const blockRootHex = payloadInput.blockRootHex;
const blockHashHex = payloadInput.getBlockHashHex();
const fork = this.config.getForkName(slot);
// 1. Emit `execution_payload_available` event at the start of import. At this point the
// payload input is already complete, so the payload and required data are available for
// payload attestation. This event only signals availability (not validity), so we can emit
// it before getting a response from the EL on whether the payload is valid or not.
if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
this.emitter.emit(routes.events.EventType.executionPayloadAvailable, {
slot,
blockRoot: blockRootHex,
});
}
// 2. Get ProtoBlock for parent root lookup
const protoBlock = this.forkChoice.getBlockHexDefaultStatus(blockRootHex);
if (!protoBlock) {
throw new PayloadError({
code: PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE,
blockRootHex,
});
}
// 3. Regenerate state for envelope verification
const blockState = await this.regen
.getBlockSlotState(protoBlock, protoBlock.slot, { dontTransferCache: true }, RegenCaller.processBlock)
.catch(() =>
// only happen at the 1st batch of skipped slot checkpoint sync
this.regen.getClosestHeadState(protoBlock));
if (blockState == null) {
throw new PayloadError({
code: PayloadErrorCode.MISS_BLOCK_STATE,
blockRootHex: protoBlock.blockRoot,
});
}
if (!isStatePostGloas(blockState)) {
throw new PayloadError({
code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR,
message: `Expected gloas+ state for payload import, got fork=${blockState.forkName}`,
});
}
// 4. Verify envelope fields against state first to fail fast before the EL + BLS work.
// When validSignature is true, gossip/API has already verified both the signature and the
// executionRequestsRoot, so we skip those checks here.
try {
verifyExecutionPayloadEnvelope(this.config, blockState, envelope, {
verifyExecutionRequestsRoot: !opts.validSignature,
});
}
catch (e) {
throw new PayloadError({
code: PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR,
message: e.message,
}, `Envelope verification error: ${e.message}`);
}
// 4a. Run EL and signature verification in parallel
const [execResult, signatureValid] = await Promise.all([
this.executionEngine.notifyNewPayload(fork, envelope.payload, payloadInput.getVersionedHashes(), envelope.parentBeaconBlockRoot, envelope.executionRequests),
opts.validSignature === true
? Promise.resolve(true)
: verifyExecutionPayloadEnvelopeSignature(this.config, blockState, this.pubkeyCache, signedEnvelope, payloadInput.proposerIndex, this.bls),
]);
// 4b. Check signature verification result
if (!signatureValid) {
throw new PayloadError({ code: PayloadErrorCode.INVALID_SIGNATURE });
}
// 4c. Handle EL response
switch (execResult.status) {
case ExecutionPayloadStatus.VALID:
break;
case ExecutionPayloadStatus.INVALID:
throw new PayloadError({
code: PayloadErrorCode.EXECUTION_ENGINE_INVALID,
execStatus: execResult.status,
errorMessage: execResult.validationError ?? "",
});
case ExecutionPayloadStatus.ACCEPTED:
case ExecutionPayloadStatus.SYNCING:
break;
case ExecutionPayloadStatus.INVALID_BLOCK_HASH:
case ExecutionPayloadStatus.ELERROR:
case ExecutionPayloadStatus.UNAVAILABLE:
throw new PayloadError({
code: PayloadErrorCode.EXECUTION_ENGINE_ERROR,
execStatus: execResult.status,
errorMessage: execResult.validationError ?? "",
});
}
// 5. Persist payload envelope to hot DB. Wait for write-queue space here to apply backpressure
// on the import pipeline during sync, then perform the write asynchronously to avoid blocking.
await this.unfinalizedPayloadEnvelopeWrites.waitForSpace();
this.unfinalizedPayloadEnvelopeWrites.push(payloadInput).catch((e) => {
if (!isQueueErrorAborted(e)) {
this.logger.error("Error pushing payload envelope to unfinalized write queue", { slot, blockRoot: blockRootHex }, e);
}
});
// 6. Update fork choice, transitions the block's PENDING variant to FULL
const execStatus = toForkChoiceExecutionStatus(execResult.status);
this.forkChoice.onExecutionPayload(blockRootHex, blockHashHex, envelope.payload.blockNumber, execStatus, dataAvailabilityStatus);
// 7. Queue notifyForkchoiceUpdate to engine api
const head = this.forkChoice.getHead();
if (!this.opts.disableImportExecutionFcU && blockRootHex === head.blockRoot) {
const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
this.executionEngine.notifyForkchoiceUpdate(fork, blockHashHex, safeBlockHash, finalizedBlockHash).catch((e) => {
if (!isErrorAborted(e) && !isQueueErrorAborted(e)) {
this.logger.error("Error pushing notifyForkchoiceUpdate()", { blockHashHex, finalizedBlockHash }, e);
}
});
}
// 8. Record metrics for payload envelope and column sources
this.metrics?.importPayload.bySource.inc({ source: payloadInput.getPayloadEnvelopeSource().source });
for (const { source } of payloadInput.getSampledColumnsWithSource()) {
this.metrics?.importPayload.columnsBySource.inc({ source });
}
// 9. Emit event after payload is fully verified and imported to fork choice, only for recent enough payloads
if (this.clock.currentSlot - slot < EVENTSTREAM_EMIT_RECENT_EXECUTION_PAYLOAD_SLOTS) {
this.emitter.emit(routes.events.EventType.executionPayload, {
slot,
builderIndex: envelope.builderIndex,
blockHash: blockHashHex,
blockRoot: blockRootHex,
executionOptimistic: execStatus === ExecutionStatus.Syncing,
});
}
this.logger.verbose("Execution payload imported", {
slot,
builderIndex: envelope.builderIndex,
blockRoot: blockRootHex,
blockHash: blockHashHex,
});
}
/**
* Process an execution payload envelope end-to-end: wait for DA, then import.
*
* Used by the PayloadEnvelopeProcessor queue (gossip / API / unknown-payload sync) — i.e.
* callers that have NOT already awaited DA themselves. Range sync's inline dispatch in
* processBlocks skips this wrapper and calls `importExecutionPayload` directly, since
* `verifyBlocksInEpoch` already awaited DA for the segment.
*/
export async function processExecutionPayload(payloadInput, signal, opts = {}) {
const { dataAvailabilityStatuses } = await verifyPayloadsDataAvailability([payloadInput], signal);
await importExecutionPayload.call(this, payloadInput, dataAvailabilityStatuses[0], opts);
}
//# sourceMappingURL=importExecutionPayload.js.map