@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
932 lines (834 loc) • 34.5 kB
text/typescript
import {ChainForkConfig} from "@lodestar/config";
import {IForkChoice, ProtoBlock, getSafeExecutionBlockHash} from "@lodestar/fork-choice";
import {
BUILDER_INDEX_SELF_BUILD,
ForkName,
ForkPostBellatrix,
ForkPostCapella,
ForkPostDeneb,
ForkPostFulu,
ForkPostGloas,
ForkPreGloas,
ForkSeq,
isForkPostAltair,
isForkPostBellatrix,
isForkPostGloas,
} from "@lodestar/params";
import {
G2_POINT_AT_INFINITY,
IBeaconStateView,
type IBeaconStateViewBellatrix,
computeTimeAtSlot,
isStatePostBellatrix,
isStatePostCapella,
isStatePostGloas,
} from "@lodestar/state-transition";
import {
BLSPubkey,
BLSSignature,
BeaconBlock,
BeaconBlockBody,
BlindedBeaconBlock,
BlindedBeaconBlockBody,
BlobsBundle,
Bytes32,
ExecutionPayload,
ExecutionPayloadHeader,
Root,
RootHex,
SSEPayloadAttributes,
Slot,
ValidatorIndex,
Wei,
altair,
capella,
deneb,
electra,
fulu,
gloas,
ssz,
} from "@lodestar/types";
import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
import {ZERO_HASH_HEX} from "../../constants/index.js";
import {numToQuantity} from "../../execution/engine/utils.js";
import {
IExecutionBuilder,
IExecutionEngine,
PayloadAttributes,
PayloadId,
getExpectedGasLimit,
} from "../../execution/index.js";
import {fromGraffitiBytes} from "../../util/graffiti.js";
import {kzg} from "../../util/kzg.js";
import type {BeaconChain} from "../chain.js";
import {CommonBlockBody} from "../interface.js";
import {validateBlobsAndKzgCommitments, validateCellsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js";
// Time to provide the EL to generate a payload from new payload id
const PAYLOAD_GENERATION_TIME_MS = 500;
export enum PayloadPreparationType {
Fresh = "Fresh",
Cached = "Cached",
Reorged = "Reorged",
Blinded = "Blinded",
}
/**
* Block production steps tracked in metrics
*/
export enum BlockProductionStep {
proposerSlashing = "proposerSlashing",
attesterSlashings = "attesterSlashings",
voluntaryExits = "voluntaryExits",
blsToExecutionChanges = "blsToExecutionChanges",
attestations = "attestations",
syncAggregate = "syncAggregate",
executionPayload = "executionPayload",
}
export type BlockAttributes = {
randaoReveal: BLSSignature;
graffiti: Bytes32;
slot: Slot;
parentBlock: ProtoBlock;
feeRecipient?: string;
};
export enum BlockType {
Full = "Full",
Blinded = "Blinded",
}
export type AssembledBodyType<T extends BlockType> = T extends BlockType.Full
? BeaconBlockBody
: BlindedBeaconBlockBody;
export type AssembledBlockType<T extends BlockType> = T extends BlockType.Full ? BeaconBlock : BlindedBeaconBlock;
export type ProduceFullGloas = {
type: BlockType.Full;
fork: ForkPostGloas;
executionPayload: ExecutionPayload<ForkPostGloas>;
executionRequests: electra.ExecutionRequests;
blobsBundle: BlobsBundle<ForkPostGloas>;
cells: fulu.Cell[][];
parentBlockRoot: Root;
};
export type ProduceFullFulu = {
type: BlockType.Full;
fork: ForkPostFulu;
executionPayload: ExecutionPayload<ForkPostFulu>;
blobsBundle: BlobsBundle<ForkPostFulu>;
cells: fulu.Cell[][];
};
export type ProduceFullDeneb = {
type: BlockType.Full;
fork: ForkName.deneb | ForkName.electra;
executionPayload: ExecutionPayload<ForkPostDeneb>;
blobsBundle: BlobsBundle<ForkPostDeneb>;
};
export type ProduceFullBellatrix = {
type: BlockType.Full;
fork: ForkName.bellatrix | ForkName.capella;
executionPayload: ExecutionPayload<ForkPostBellatrix>;
};
export type ProduceFullPhase0 = {
type: BlockType.Full;
fork: ForkName.phase0 | ForkName.altair;
};
export type ProduceBlinded = {
type: BlockType.Blinded;
fork: ForkName;
};
// The results of block production returned by `produceBlockBody`
// The types are defined separately so typecasting can be used
/** The result of local block production, everything that's not the block itself */
export type ProduceResult =
| ProduceFullGloas
| ProduceFullFulu
| ProduceFullDeneb
| ProduceFullBellatrix
| ProduceFullPhase0
| ProduceBlinded;
export async function produceBlockBody<T extends BlockType>(
this: BeaconChain,
blockType: T,
currentState: IBeaconStateView,
blockAttr: BlockAttributes & {
proposerIndex: ValidatorIndex;
proposerPubKey: BLSPubkey;
commonBlockBodyPromise: Promise<CommonBlockBody>;
}
): Promise<{
body: AssembledBodyType<T>;
produceResult: ProduceResult;
executionPayloadValue: Wei;
shouldOverrideBuilder?: boolean;
}> {
const {
slot: blockSlot,
feeRecipient: requestedFeeRecipient,
parentBlock,
proposerIndex,
proposerPubKey,
commonBlockBodyPromise,
} = blockAttr;
let executionPayloadValue: Wei;
let blockBody: AssembledBodyType<T>;
const parentBlockRoot = fromHex(parentBlock.blockRoot);
// even though shouldOverrideBuilder is relevant for the engine response, for simplicity of typing
// we just return it undefined for the builder which anyway doesn't get consumed downstream
let shouldOverrideBuilder: boolean | undefined;
const fork = this.config.getForkName(blockSlot);
const produceResult = {
type: blockType,
fork,
} as ProduceResult;
const logMeta: Record<string, string | number | bigint> = {
fork,
blockType,
slot: blockSlot,
};
this.logger.verbose("Producing beacon block body", logMeta);
if (isForkPostGloas(fork)) {
if (!isStatePostGloas(currentState)) {
throw new Error("Expected Gloas state for Gloas block production");
}
// TODO GLOAS: support non self-building here, the block type differentiation between
// full and blinded no longer makes sense in gloas, it might be a good idea to move
// this into a completely separate function and have pre/post gloas more separated
const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();
this.logger.verbose("Preparing execution payload from engine", {
slot: blockSlot,
parentBlockRoot: toRootHex(parentBlockRoot),
feeRecipient,
});
// Get execution payload from EL
let parentBlockHash: Bytes32;
let parentExecutionRequests: electra.ExecutionRequests;
// Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below
let stateAfterParentPayload: IBeaconStateViewBellatrix = currentState;
const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot));
if (isExtendingPayload) {
parentBlockHash = currentState.latestExecutionPayloadBid.blockHash;
parentExecutionRequests = await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot);
stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests);
} else {
parentBlockHash = currentState.latestExecutionPayloadBid.parentBlockHash;
parentExecutionRequests = ssz.electra.ExecutionRequests.defaultValue();
}
const prepareRes = await prepareExecutionPayload(
this,
this.logger,
fork,
parentBlockRoot,
parentBlockHash,
safeBlockHash,
finalizedBlockHash ?? ZERO_HASH_HEX,
stateAfterParentPayload,
feeRecipient
);
const {prepType, payloadId} = prepareRes;
Object.assign(logMeta, {executionPayloadPrepType: prepType});
if (prepType !== PayloadPreparationType.Cached) {
await sleep(PAYLOAD_GENERATION_TIME_MS);
}
this.logger.verbose("Fetching execution payload from engine", {slot: blockSlot, payloadId});
const payloadRes = await this.executionEngine.getPayload(fork, payloadId);
endExecutionPayload?.({step: BlockProductionStep.executionPayload});
const {executionPayload, blobsBundle, executionRequests} = payloadRes;
executionPayloadValue = payloadRes.executionPayloadValue;
shouldOverrideBuilder = payloadRes.shouldOverrideBuilder;
if (blobsBundle === undefined) {
throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
}
if (executionRequests === undefined) {
throw Error(`Missing executionRequests response from getPayload at fork=${fork}`);
}
const cells = blobsBundle.blobs.map((blob) => kzg.computeCells(blob));
if (this.opts.sanityCheckExecutionEngineBlobs) {
await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells);
}
// Create self-build execution payload bid
const bid: gloas.ExecutionPayloadBid = {
parentBlockHash,
parentBlockRoot,
blockHash: executionPayload.blockHash,
prevRandao: currentState.getRandaoMix(currentState.epoch),
feeRecipient: executionPayload.feeRecipient,
gasLimit: BigInt(executionPayload.gasLimit),
builderIndex: BUILDER_INDEX_SELF_BUILD,
slot: blockSlot,
value: 0,
executionPayment: 0,
blobKzgCommitments: blobsBundle.commitments,
executionRequestsRoot: ssz.electra.ExecutionRequests.hashTreeRoot(executionRequests),
};
const signedBid: gloas.SignedExecutionPayloadBid = {
message: bid,
signature: G2_POINT_AT_INFINITY,
};
const commonBlockBody = await commonBlockBodyPromise;
const gloasBody = Object.assign({}, commonBlockBody) as gloas.BeaconBlockBody;
gloasBody.signedExecutionPayloadBid = signedBid;
gloasBody.payloadAttestations = this.payloadAttestationPool.getPayloadAttestationsForBlock(
parentBlock.blockRoot,
blockSlot - 1
);
gloasBody.parentExecutionRequests = parentExecutionRequests;
// Drop voluntary exits that parent_execution_requests have invalidated (e.g. a withdrawal
// request initiating an exit on the same validator). Op pool selected against the unapplied
// state, so re-validate against the post-apply state to avoid producing an invalid block.
if (isExtendingPayload && commonBlockBody.voluntaryExits.length > 0) {
gloasBody.voluntaryExits = commonBlockBody.voluntaryExits.filter((signedVoluntaryExit) =>
stateAfterParentPayload.isValidVoluntaryExit(signedVoluntaryExit, false)
);
}
blockBody = gloasBody as AssembledBodyType<T>;
// Store execution payload data required to construct execution payload envelope later
const gloasResult = produceResult as ProduceFullGloas;
gloasResult.executionPayload = executionPayload as ExecutionPayload<ForkPostGloas>;
gloasResult.executionRequests = executionRequests;
gloasResult.blobsBundle = blobsBundle;
gloasResult.cells = cells;
gloasResult.parentBlockRoot = fromHex(parentBlock.blockRoot);
const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
this.logger.verbose("Produced block with self-build bid", {
slot: blockSlot,
executionPayloadValue,
prepType,
payloadId,
fetchedTime,
executionBlockHash: toRootHex(executionPayload.blockHash),
blobs: blobsBundle.commitments.length,
});
Object.assign(logMeta, {
transactions: executionPayload.transactions.length,
blobs: blobsBundle.commitments.length,
shouldOverrideBuilder,
});
} else if (isForkPostBellatrix(fork)) {
if (!isStatePostBellatrix(currentState)) {
throw new Error("Expected Bellatrix state for execution block production");
}
const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice);
const finalizedBlockHash = this.forkChoice.getFinalizedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
const feeRecipientType = requestedFeeRecipient
? "requested"
: this.beaconProposerCache.get(proposerIndex)
? "cached"
: "default";
Object.assign(logMeta, {feeRecipientType, feeRecipient});
if (blockType === BlockType.Blinded) {
if (!this.executionBuilder) throw Error("External builder not configured");
const executionBuilder = this.executionBuilder;
const builderPromise = (async () => {
const endExecutionPayloadHeader = this.metrics?.builderBlockProductionTimeSteps.startTimer();
// This path will not be used in the production, but is here just for merge mock
// tests because merge-mock requires an fcU to be issued prior to fetch payload
// header.
if (executionBuilder.issueLocalFcUWithFeeRecipient !== undefined) {
await prepareExecutionPayload(
this,
this.logger,
fork,
parentBlockRoot,
currentState.latestExecutionPayloadHeader.blockHash,
safeBlockHash,
finalizedBlockHash ?? ZERO_HASH_HEX,
currentState,
executionBuilder.issueLocalFcUWithFeeRecipient
);
}
// For MeV boost integration, this is where the execution header will be
// fetched from the payload id and a blinded block will be produced instead of
// fullblock for the validator to sign
this.logger.verbose("Fetching execution payload header from builder", {
slot: blockSlot,
proposerPubKey: toHex(proposerPubKey),
});
const headerRes = await prepareExecutionPayloadHeader(this, fork, currentState, proposerPubKey);
endExecutionPayloadHeader?.({
step: BlockProductionStep.executionPayload,
});
return headerRes;
})();
const [builderRes, commonBlockBody] = await Promise.all([builderPromise, commonBlockBodyPromise]);
blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<BlockType.Blinded>;
(blockBody as BlindedBeaconBlockBody).executionPayloadHeader = builderRes.header;
executionPayloadValue = builderRes.executionPayloadValue;
const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
const prepType = PayloadPreparationType.Blinded;
this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
this.logger.verbose("Fetched execution payload header from builder", {
slot: blockSlot,
executionPayloadValue,
prepType,
fetchedTime,
});
const targetGasLimit = executionBuilder.getValidatorRegistration(proposerPubKey)?.gasLimit;
if (!targetGasLimit) {
// This should only happen if cache was cleared due to restart of beacon node
this.logger.warn("Failed to get validator registration, could not check header gas limit", {
slot: blockSlot,
proposerIndex,
proposerPubKey: toPubkeyHex(proposerPubKey),
});
} else {
const headerGasLimit = builderRes.header.gasLimit;
const parentGasLimit = currentState.latestExecutionPayloadHeader.gasLimit;
const expectedGasLimit = getExpectedGasLimit(parentGasLimit, targetGasLimit);
const lowerBound = Math.min(parentGasLimit, expectedGasLimit);
const upperBound = Math.max(parentGasLimit, expectedGasLimit);
if (headerGasLimit < lowerBound || headerGasLimit > upperBound) {
throw Error(
`Header gas limit ${headerGasLimit} is outside of acceptable range [${lowerBound}, ${upperBound}]`
);
}
if (headerGasLimit !== expectedGasLimit) {
this.logger.warn("Header gas limit does not match expected value", {
slot: blockSlot,
headerGasLimit,
expectedGasLimit,
parentGasLimit,
targetGasLimit,
});
}
}
if (ForkSeq[fork] >= ForkSeq.deneb) {
const {blobKzgCommitments} = builderRes;
if (blobKzgCommitments === undefined) {
throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
}
(blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
Object.assign(logMeta, {blobs: blobKzgCommitments.length});
}
if (ForkSeq[fork] >= ForkSeq.electra) {
const {executionRequests} = builderRes;
if (executionRequests === undefined) {
throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
}
(blockBody as electra.BlindedBeaconBlockBody).executionRequests = executionRequests;
}
}
// blockType === BlockType.Full
else {
// enginePromise only supports pre-gloas
const enginePromise = (async () => {
const endExecutionPayload = this.metrics?.executionBlockProductionTimeSteps.startTimer();
this.logger.verbose("Preparing execution payload from engine", {
slot: blockSlot,
parentBlockRoot: toRootHex(parentBlockRoot),
feeRecipient,
});
// https://github.com/ethereum/consensus-specs/blob/v1.6.1/specs/deneb/validator.md#constructing-the-beaconblockbody
const prepareRes = await prepareExecutionPayload(
this,
this.logger,
fork,
parentBlockRoot,
currentState.latestExecutionPayloadHeader.blockHash,
safeBlockHash,
finalizedBlockHash ?? ZERO_HASH_HEX,
currentState,
feeRecipient
);
const {prepType, payloadId} = prepareRes;
Object.assign(logMeta, {executionPayloadPrepType: prepType});
if (prepType !== PayloadPreparationType.Cached) {
// Wait for 500ms to allow EL to add some txs to the payload
// the pitfalls of this have been put forward here, but 500ms delay for block proposal
// seems marginal even with unhealthy network
//
// See: https://discord.com/channels/595666850260713488/892088344438255616/1009882079632314469
await sleep(PAYLOAD_GENERATION_TIME_MS);
}
this.logger.verbose("Fetching execution payload from engine", {slot: blockSlot, payloadId});
const payloadRes = await this.executionEngine.getPayload(fork, payloadId);
endExecutionPayload?.({
step: BlockProductionStep.executionPayload,
});
return {...prepareRes, ...payloadRes};
})().catch((e) => {
this.metrics?.blockPayload.payloadFetchErrors.inc();
throw e;
});
const [engineRes, commonBlockBody] = await Promise.all([enginePromise, commonBlockBodyPromise]);
blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<BlockType.Blinded>;
{
const {prepType, payloadId, executionPayload, blobsBundle, executionRequests} = engineRes;
shouldOverrideBuilder = engineRes.shouldOverrideBuilder;
(blockBody as BeaconBlockBody<ForkPostBellatrix & ForkPreGloas>).executionPayload = executionPayload;
(produceResult as ProduceFullBellatrix).executionPayload = executionPayload;
executionPayloadValue = engineRes.executionPayloadValue;
Object.assign(logMeta, {transactions: executionPayload.transactions.length, shouldOverrideBuilder});
const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
this.logger.verbose("Fetched execution payload from engine", {
slot: blockSlot,
executionPayloadValue,
prepType,
payloadId,
fetchedTime,
executionHeadBlockHash: toRootHex(engineRes.executionPayload.blockHash),
});
if (executionPayload.transactions.length === 0) {
this.metrics?.blockPayload.emptyPayloads.inc({prepType});
}
if (ForkSeq[fork] >= ForkSeq.fulu) {
if (blobsBundle === undefined) {
throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
}
// NOTE: Even though the fulu.BlobsBundle type is superficially the same as deneb.BlobsBundle, it is NOT.
// In fulu, proofs are _cell_ proofs, vs in deneb they are _blob_ proofs.
const timer = this?.metrics?.peerDas.dataColumnSidecarComputationTime.startTimer();
const cells = blobsBundle.blobs.map((blob) => kzg.computeCells(blob));
timer?.();
if (this.opts.sanityCheckExecutionEngineBlobs) {
const validationTimer = this.metrics?.peerDas.kzgVerificationDataColumnBatchTime.startTimer();
try {
await validateCellsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, cells);
} finally {
validationTimer?.();
}
}
(blockBody as deneb.BeaconBlockBody).blobKzgCommitments = blobsBundle.commitments;
(produceResult as ProduceFullFulu).blobsBundle = blobsBundle;
(produceResult as ProduceFullFulu).cells = cells;
Object.assign(logMeta, {blobs: blobsBundle.commitments.length});
} else if (ForkSeq[fork] >= ForkSeq.deneb) {
if (blobsBundle === undefined) {
throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
}
if (this.opts.sanityCheckExecutionEngineBlobs) {
await validateBlobsAndKzgCommitments(blobsBundle.commitments, blobsBundle.proofs, blobsBundle.blobs);
}
(blockBody as deneb.BeaconBlockBody).blobKzgCommitments = blobsBundle.commitments;
(produceResult as ProduceFullDeneb).blobsBundle = blobsBundle;
Object.assign(logMeta, {blobs: blobsBundle.commitments.length});
}
if (ForkSeq[fork] >= ForkSeq.electra) {
if (executionRequests === undefined) {
throw Error(`Missing executionRequests response from getPayload at fork=${fork}`);
}
(blockBody as electra.BeaconBlockBody).executionRequests = executionRequests;
}
}
}
} else {
const commonBlockBody = await commonBlockBodyPromise;
blockBody = Object.assign({}, commonBlockBody) as AssembledBodyType<T>;
executionPayloadValue = BigInt(0);
}
const {graffiti, attestations, deposits, voluntaryExits, attesterSlashings, proposerSlashings} = blockBody;
Object.assign(logMeta, {
graffiti: fromGraffitiBytes(graffiti),
attestations: attestations.length,
deposits: deposits.length,
voluntaryExits: voluntaryExits.length,
attesterSlashings: attesterSlashings.length,
proposerSlashings: proposerSlashings.length,
});
if (isForkPostAltair(fork)) {
const {syncAggregate} = blockBody as altair.BeaconBlockBody;
Object.assign(logMeta, {
syncAggregateParticipants: syncAggregate.syncCommitteeBits.getTrueBitIndexes().length,
});
}
if (ForkSeq[fork] >= ForkSeq.gloas) {
const {blsToExecutionChanges, payloadAttestations} = blockBody as BeaconBlockBody<ForkPostGloas>;
Object.assign(logMeta, {
blsToExecutionChanges: blsToExecutionChanges.length,
payloadAttestations: payloadAttestations.length,
});
} else if (ForkSeq[fork] >= ForkSeq.capella) {
const {blsToExecutionChanges, executionPayload} = blockBody as BeaconBlockBody<ForkPostCapella & ForkPreGloas>;
Object.assign(logMeta, {
blsToExecutionChanges: blsToExecutionChanges.length,
});
// withdrawals are only available in full body
if (blockType === BlockType.Full) {
Object.assign(logMeta, {
withdrawals: executionPayload.withdrawals.length,
});
}
}
Object.assign(logMeta, {executionPayloadValue});
this.logger.verbose("Produced beacon block body", logMeta);
return {body: blockBody as AssembledBodyType<T>, produceResult, executionPayloadValue, shouldOverrideBuilder};
}
/**
* Produce ExecutionPayload for post-merge.
*/
export async function prepareExecutionPayload(
chain: {
executionEngine: IExecutionEngine;
config: ChainForkConfig;
},
logger: Logger,
fork: ForkPostBellatrix,
parentBlockRoot: Root,
parentBlockHash: Bytes32,
safeBlockHash: RootHex,
finalizedBlockHash: RootHex,
/**
* Post-gloas, when extending a full parent, callers must apply
* parent execution payload first (see `withParentPayloadApplied`).
*/
state: IBeaconStateViewBellatrix,
suggestedFeeRecipient: string
): Promise<{prepType: PayloadPreparationType; payloadId: PayloadId}> {
const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime);
const prevRandao = state.getRandaoMix(state.epoch);
const payloadIdCached = chain.executionEngine.payloadIdCache.get({
headBlockHash: toRootHex(parentBlockHash),
finalizedBlockHash,
timestamp: numToQuantity(timestamp),
prevRandao: toHex(prevRandao),
suggestedFeeRecipient,
});
// prepareExecutionPayload will throw error via notifyForkchoiceUpdate if
// the EL returns Syncing on this request to prepare a payload
// TODO: Handle only this case, DO NOT put a generic try / catch that discards all errors
let payloadId: PayloadId | null;
let prepType: PayloadPreparationType;
if (payloadIdCached) {
payloadId = payloadIdCached;
prepType = PayloadPreparationType.Cached;
} else {
// If there was a payload assigned to this timestamp, it would imply that there some sort
// of payload reorg, i.e. head, fee recipient or any other fcu param changed
if (chain.executionEngine.payloadIdCache.hasPayload({timestamp: numToQuantity(timestamp)})) {
prepType = PayloadPreparationType.Reorged;
} else {
prepType = PayloadPreparationType.Fresh;
}
const attributes: PayloadAttributes = preparePayloadAttributes(fork, chain, {
prepareState: state,
prepareSlot: state.slot,
parentBlockRoot,
parentBlockHash,
feeRecipient: suggestedFeeRecipient,
});
payloadId = await chain.executionEngine.notifyForkchoiceUpdate(
fork,
toRootHex(parentBlockHash),
safeBlockHash,
finalizedBlockHash,
attributes
);
logger.verbose("Prepared payload id from execution engine", {payloadId});
}
// Should never happen, notifyForkchoiceUpdate() with payload attributes always
// returns payloadId
if (payloadId === null) {
throw Error("notifyForkchoiceUpdate returned payloadId null");
}
// We are only returning payloadId here because prepareExecutionPayload is also called from
// prepareNextSlot, which is an advance call to execution engine to start building payload
// Actual payload isn't produced till getPayload is called.
return {payloadId, prepType};
}
async function prepareExecutionPayloadHeader(
chain: {
executionBuilder?: IExecutionBuilder;
config: ChainForkConfig;
},
fork: ForkPostBellatrix,
state: IBeaconStateViewBellatrix,
proposerPubKey: BLSPubkey
): Promise<{
header: ExecutionPayloadHeader;
executionPayloadValue: Wei;
blobKzgCommitments?: deneb.BlobKzgCommitments;
executionRequests?: electra.ExecutionRequests;
}> {
if (!chain.executionBuilder) {
throw Error("executionBuilder required");
}
const parentHash = state.latestExecutionPayloadHeader.blockHash;
return chain.executionBuilder.getHeader(fork, state.slot, parentHash, proposerPubKey);
}
export function getPayloadAttributesForSSE(
fork: ForkPostBellatrix,
chain: {
config: ChainForkConfig;
forkChoice: IForkChoice;
},
{
prepareState,
prepareSlot,
parentBlockRoot,
parentBlockHash,
feeRecipient,
}: {
/**
* Post-gloas, when extending a full parent, callers must apply
* parent execution payload first (see `withParentPayloadApplied`).
*/
prepareState: IBeaconStateViewBellatrix;
prepareSlot: Slot;
parentBlockRoot: Root;
parentBlockHash: Bytes32;
feeRecipient: string;
}
): SSEPayloadAttributes {
const payloadAttributes = preparePayloadAttributes(fork, chain, {
prepareState,
prepareSlot,
parentBlockRoot,
parentBlockHash,
feeRecipient,
});
let parentBlockNumber: number;
if (isForkPostGloas(fork)) {
const parentBlock = chain.forkChoice.getBlockHexAndBlockHash(
toRootHex(parentBlockRoot),
toRootHex(parentBlockHash)
);
if (parentBlock?.executionPayloadBlockHash == null) {
throw Error(`Parent block not found in fork choice root=${toRootHex(parentBlockRoot)}`);
}
parentBlockNumber = parentBlock.executionPayloadNumber;
} else {
parentBlockNumber = prepareState.payloadBlockNumber;
}
const ssePayloadAttributes: SSEPayloadAttributes = {
proposerIndex: prepareState.getBeaconProposer(prepareSlot),
proposalSlot: prepareSlot,
parentBlockNumber,
parentBlockRoot,
parentBlockHash,
payloadAttributes,
};
return ssePayloadAttributes;
}
function preparePayloadAttributes(
fork: ForkPostBellatrix,
chain: {
config: ChainForkConfig;
},
{
prepareState,
prepareSlot,
parentBlockRoot,
parentBlockHash,
feeRecipient,
}: {
/**
* Post-gloas, when extending a full parent, callers must apply
* parent execution payload first (see `withParentPayloadApplied`).
*/
prepareState: IBeaconStateViewBellatrix;
prepareSlot: Slot;
parentBlockRoot: Root;
parentBlockHash: Bytes32;
feeRecipient: string;
}
): SSEPayloadAttributes["payloadAttributes"] {
const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
const prevRandao = prepareState.getRandaoMix(prepareState.epoch);
const payloadAttributes = {
timestamp,
prevRandao,
suggestedFeeRecipient: feeRecipient,
};
if (ForkSeq[fork] >= ForkSeq.capella) {
if (!isStatePostCapella(prepareState)) {
throw new Error("Expected Capella state for withdrawals");
}
if (isStatePostGloas(prepareState)) {
const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
if (isExtendingPayload) {
// applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch
// here means the caller did not apply parent payload to prepareState
if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) {
throw new Error("Expected state with parent execution payload applied for withdrawals");
}
(payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
prepareState.getExpectedWithdrawals().expectedWithdrawals;
} else {
// When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
// already deducted from CL balances but never credited on the EL (the envelope
// was not delivered). The next payload must carry those same withdrawals to
// restore CL/EL consistency, otherwise validators permanently lose that balance.
(payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
prepareState.payloadExpectedWithdrawals;
}
} else {
// withdrawals logic is now fork aware as it changes on electra fork post capella
(payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
prepareState.getExpectedWithdrawals().expectedWithdrawals;
}
}
if (ForkSeq[fork] >= ForkSeq.deneb) {
(payloadAttributes as deneb.SSEPayloadAttributes["payloadAttributes"]).parentBeaconBlockRoot = parentBlockRoot;
}
if (ForkSeq[fork] >= ForkSeq.gloas) {
(payloadAttributes as gloas.SSEPayloadAttributes["payloadAttributes"]).slotNumber = prepareSlot;
}
return payloadAttributes;
}
export async function produceCommonBlockBody<T extends BlockType>(
this: BeaconChain,
blockType: T,
currentState: IBeaconStateView,
{randaoReveal, graffiti, slot, parentBlock}: BlockAttributes
): Promise<CommonBlockBody> {
const stepsMetrics =
blockType === BlockType.Full
? this.metrics?.executionBlockProductionTimeSteps
: this.metrics?.builderBlockProductionTimeSteps;
const fork = this.config.getForkName(slot);
// TODO:
// Iterate through the naive aggregation pool and ensure all the attestations from there
// are included in the operation pool.
// for (const attestation of db.attestationPool.getAll()) {
// try {
// opPool.insertAttestation(attestation);
// } catch (e) {
// // Don't stop block production if there's an error, just create a log.
// logger.error("Attestation did not transfer to op pool", {}, e);
// }
// }
const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] =
this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics);
const endAttestations = stepsMetrics?.startTimer();
const attestations = this.aggregatedAttestationPool.getAttestationsForBlock(
fork,
this.forkChoice,
this.shufflingCache,
currentState
);
endAttestations?.({
step: BlockProductionStep.attestations,
});
const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = {
randaoReveal,
graffiti,
// Eth1 data voting is no longer required since electra
eth1Data: currentState.eth1Data,
proposerSlashings,
attesterSlashings,
attestations,
// Since electra, deposits are processed by the execution layer,
// we no longer support handling deposits from earlier forks.
deposits: [],
voluntaryExits,
};
if (ForkSeq[fork] >= ForkSeq.capella) {
(blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges;
}
const endSyncAggregate = stepsMetrics?.startTimer();
if (ForkSeq[fork] >= ForkSeq.altair) {
const parentBlockRoot = fromHex(parentBlock.blockRoot);
const previousSlot = slot - 1;
const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot);
this.metrics?.production.producedSyncAggregateParticipants.observe(
syncAggregate.syncCommitteeBits.getTrueBitIndexes().length
);
(blockBody as CommonBlockBody).syncAggregate = syncAggregate;
}
endSyncAggregate?.({
step: BlockProductionStep.syncAggregate,
});
return blockBody as CommonBlockBody;
}