@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
486 lines • 25.7 kB
JavaScript
import { ForkSeq, isForkPostAltair, isForkPostBellatrix } from "@lodestar/params";
import { computeTimeAtSlot, getCurrentEpoch, getExpectedWithdrawals, getRandaoMix, isMergeTransitionComplete, } from "@lodestar/state-transition";
import { ssz, sszTypesFor, } from "@lodestar/types";
import { sleep, toHex, toPubkeyHex, toRootHex } from "@lodestar/utils";
import { ZERO_HASH, ZERO_HASH_HEX } from "../../constants/index.js";
import { numToQuantity } from "../../eth1/provider/utils.js";
import { getExpectedGasLimit, } from "../../execution/index.js";
import { fromGraffitiBytes } from "../../util/graffiti.js";
import { validateBlobsAndKzgCommitments } from "./validateBlobsAndKzgCommitments.js";
// Time to provide the EL to generate a payload from new payload id
const PAYLOAD_GENERATION_TIME_MS = 500;
export var PayloadPreparationType;
(function (PayloadPreparationType) {
PayloadPreparationType["Fresh"] = "Fresh";
PayloadPreparationType["Cached"] = "Cached";
PayloadPreparationType["Reorged"] = "Reorged";
PayloadPreparationType["Blinded"] = "Blinded";
})(PayloadPreparationType || (PayloadPreparationType = {}));
/**
* Block production steps tracked in metrics
*/
export var BlockProductionStep;
(function (BlockProductionStep) {
BlockProductionStep["proposerSlashing"] = "proposerSlashing";
BlockProductionStep["attesterSlashings"] = "attesterSlashings";
BlockProductionStep["voluntaryExits"] = "voluntaryExits";
BlockProductionStep["blsToExecutionChanges"] = "blsToExecutionChanges";
BlockProductionStep["attestations"] = "attestations";
BlockProductionStep["eth1DataAndDeposits"] = "eth1DataAndDeposits";
BlockProductionStep["syncAggregate"] = "syncAggregate";
BlockProductionStep["executionPayload"] = "executionPayload";
})(BlockProductionStep || (BlockProductionStep = {}));
export var BlockType;
(function (BlockType) {
BlockType["Full"] = "Full";
BlockType["Blinded"] = "Blinded";
})(BlockType || (BlockType = {}));
export var BlobsResultType;
(function (BlobsResultType) {
BlobsResultType[BlobsResultType["preDeneb"] = 0] = "preDeneb";
BlobsResultType[BlobsResultType["produced"] = 1] = "produced";
BlobsResultType[BlobsResultType["blinded"] = 2] = "blinded";
})(BlobsResultType || (BlobsResultType = {}));
export async function produceBlockBody(blockType, currentState, blockAttr) {
const { slot: blockSlot, feeRecipient: requestedFeeRecipient, parentBlockRoot, proposerIndex, proposerPubKey, commonBlockBodyPromise, } = blockAttr;
// Type-safe for blobs variable. Translate 'null' value into 'preDeneb' enum
// TODO: Not ideal, but better than just using null.
// TODO: Does not guarantee that preDeneb enum goes with a preDeneb block
let blobsResult;
let executionPayloadValue;
let blockBody;
// 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;
const fork = currentState.config.getForkName(blockSlot);
const logMeta = {
fork,
blockType,
slot: blockSlot,
};
this.logger.verbose("Producing beacon block body", logMeta);
if (isForkPostBellatrix(fork)) {
const safeBlockHash = this.forkChoice.getJustifiedBlock().executionPayloadBlockHash ?? ZERO_HASH_HEX;
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("Execution Builder not available");
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, 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 ?? produceCommonBlockBody.call(this, blockType, currentState, blockAttr),
]);
blockBody = Object.assign({}, commonBlockBody);
blockBody.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.blobKzgCommitments = blobKzgCommitments;
blobsResult = { type: BlobsResultType.blinded };
Object.assign(logMeta, { blobs: blobKzgCommitments.length });
}
else {
blobsResult = { type: BlobsResultType.preDeneb };
}
if (ForkSeq[fork] >= ForkSeq.electra) {
const { executionRequests } = builderRes;
if (executionRequests === undefined) {
throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
}
blockBody.executionRequests = executionRequests;
}
}
// blockType === BlockType.Full
else {
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/dev/specs/deneb/validator.md#constructing-the-beaconblockbody
const prepareRes = await prepareExecutionPayload(this, this.logger, fork, parentBlockRoot, safeBlockHash, finalizedBlockHash ?? ZERO_HASH_HEX, currentState, feeRecipient);
if (prepareRes.isPremerge) {
return {
...prepareRes,
executionPayload: sszTypesFor(fork).ExecutionPayload.defaultValue(),
executionPayloadValue: BigInt(0),
};
}
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) => {
// catch payload fetch here, because there is still a recovery path possible if we
// are pre-merge. We don't care the same for builder segment as the execution block
// will takeover if the builder flow was activated and errors
this.metrics?.blockPayload.payloadFetchErrors.inc();
if (!isMergeTransitionComplete(currentState)) {
this.logger?.warn("Fetch payload from the execution failed, however since we are still pre-merge proceeding with an empty one.", {}, e);
// ok we don't have an execution payload here, so we can assign an empty one
// if pre-merge
return {
isPremerge: true,
executionPayload: sszTypesFor(fork).ExecutionPayload.defaultValue(),
executionPayloadValue: BigInt(0),
};
}
// since merge transition is complete, we need a valid payload even if with an
// empty (transactions) one. defaultValue isn't gonna cut it!
throw e;
});
const [engineRes, commonBlockBody] = await Promise.all([
enginePromise,
commonBlockBodyPromise ?? produceCommonBlockBody.call(this, blockType, currentState, blockAttr),
]);
blockBody = Object.assign({}, commonBlockBody);
if (engineRes.isPremerge) {
blockBody.executionPayload = engineRes.executionPayload;
blobsResult = { type: BlobsResultType.preDeneb };
executionPayloadValue = engineRes.executionPayloadValue;
}
else {
const { prepType, payloadId, executionPayload, blobsBundle, executionRequests } = engineRes;
shouldOverrideBuilder = engineRes.shouldOverrideBuilder;
blockBody.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.deneb) {
if (blobsBundle === undefined) {
throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
}
if (this.opts.sanityCheckExecutionEngineBlobs) {
validateBlobsAndKzgCommitments(executionPayload, blobsBundle);
}
blockBody.blobKzgCommitments = blobsBundle.commitments;
const blockHash = toRootHex(executionPayload.blockHash);
const contents = { kzgProofs: blobsBundle.proofs, blobs: blobsBundle.blobs };
blobsResult = { type: BlobsResultType.produced, contents, blockHash };
Object.assign(logMeta, { blobs: blobsBundle.commitments.length });
}
else {
blobsResult = { type: BlobsResultType.preDeneb };
}
if (ForkSeq[fork] >= ForkSeq.electra) {
if (executionRequests === undefined) {
throw Error(`Missing executionRequests response from getPayload at fork=${fork}`);
}
blockBody.executionRequests = executionRequests;
}
}
}
}
else {
const commonBlockBody = await (commonBlockBodyPromise ??
produceCommonBlockBody.call(this, blockType, currentState, blockAttr));
blockBody = Object.assign({}, commonBlockBody);
blobsResult = { type: BlobsResultType.preDeneb };
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;
Object.assign(logMeta, {
syncAggregateParticipants: syncAggregate.syncCommitteeBits.getTrueBitIndexes().length,
});
}
if (ForkSeq[fork] >= ForkSeq.capella) {
const { blsToExecutionChanges, executionPayload } = blockBody;
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, blobs: blobsResult, executionPayloadValue, shouldOverrideBuilder };
}
/**
* Produce ExecutionPayload for pre-merge, merge, and post-merge.
*
* Expects `eth1MergeBlockFinder` to be actively searching for blocks well in advance to being called.
*
* @returns PayloadId = pow block found, null = pow NOT found
*/
export async function prepareExecutionPayload(chain, logger, fork, parentBlockRoot, safeBlockHash, finalizedBlockHash, state, suggestedFeeRecipient) {
const parentHashRes = await getExecutionPayloadParentHash(chain, state);
if (parentHashRes.isPremerge) {
// Return null only if the execution is pre-merge
return { isPremerge: true };
}
const { parentHash } = parentHashRes;
const timestamp = computeTimeAtSlot(chain.config, state.slot, state.genesisTime);
const prevRandao = getRandaoMix(state, state.epochCtx.epoch);
const payloadIdCached = chain.executionEngine.payloadIdCache.get({
headBlockHash: toRootHex(parentHash),
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;
let prepType;
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 = preparePayloadAttributes(fork, chain, {
prepareState: state,
prepareSlot: state.slot,
parentBlockRoot,
feeRecipient: suggestedFeeRecipient,
});
payloadId = await chain.executionEngine.notifyForkchoiceUpdate(fork, toRootHex(parentHash), 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 { isPremerge: false, payloadId, prepType };
}
async function prepareExecutionPayloadHeader(chain, fork, state, proposerPubKey) {
if (!chain.executionBuilder) {
throw Error("executionBuilder required");
}
const parentHashRes = await getExecutionPayloadParentHash(chain, state);
if (parentHashRes.isPremerge) {
throw Error("Execution builder disabled pre-merge");
}
const { parentHash } = parentHashRes;
return chain.executionBuilder.getHeader(fork, state.slot, parentHash, proposerPubKey);
}
export async function getExecutionPayloadParentHash(chain, state) {
// Use different POW block hash parent for block production based on merge status.
// Returned value of null == using an empty ExecutionPayload value
if (isMergeTransitionComplete(state)) {
// Post-merge, normal payload
return { isPremerge: false, parentHash: state.latestExecutionPayloadHeader.blockHash };
}
if (!ssz.Root.equals(chain.config.TERMINAL_BLOCK_HASH, ZERO_HASH) &&
getCurrentEpoch(state) < chain.config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH) {
throw new Error(`InvalidMergeTBH epoch: expected >= ${chain.config.TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH}, actual: ${getCurrentEpoch(state)}`);
}
const terminalPowBlockHash = await chain.eth1.getTerminalPowBlock();
if (terminalPowBlockHash === null) {
// Pre-merge, no prepare payload call is needed
return { isPremerge: true };
}
// Signify merge via producing on top of the last PoW block
return { isPremerge: false, parentHash: terminalPowBlockHash };
}
export async function getPayloadAttributesForSSE(fork, chain, { prepareState, prepareSlot, parentBlockRoot, feeRecipient, }) {
const parentHashRes = await getExecutionPayloadParentHash(chain, prepareState);
if (!parentHashRes.isPremerge) {
const { parentHash } = parentHashRes;
const payloadAttributes = preparePayloadAttributes(fork, chain, {
prepareState,
prepareSlot,
parentBlockRoot,
feeRecipient,
});
const ssePayloadAttributes = {
proposerIndex: prepareState.epochCtx.getBeaconProposer(prepareSlot),
proposalSlot: prepareSlot,
parentBlockNumber: prepareState.latestExecutionPayloadHeader.blockNumber,
parentBlockRoot,
parentBlockHash: parentHash,
payloadAttributes,
};
return ssePayloadAttributes;
}
throw Error("The execution is still pre-merge");
}
function preparePayloadAttributes(fork, chain, { prepareState, prepareSlot, parentBlockRoot, feeRecipient, }) {
const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
const prevRandao = getRandaoMix(prepareState, prepareState.epochCtx.epoch);
const payloadAttributes = {
timestamp,
prevRandao,
suggestedFeeRecipient: feeRecipient,
};
if (ForkSeq[fork] >= ForkSeq.capella) {
// withdrawals logic is now fork aware as it changes on electra fork post capella
payloadAttributes.withdrawals = getExpectedWithdrawals(ForkSeq[fork], prepareState).withdrawals;
}
if (ForkSeq[fork] >= ForkSeq.deneb) {
payloadAttributes.parentBeaconBlockRoot = parentBlockRoot;
}
return payloadAttributes;
}
export async function produceCommonBlockBody(blockType, currentState, { randaoReveal, graffiti, slot, parentSlot, parentBlockRoot, }) {
const stepsMetrics = blockType === BlockType.Full
? this.metrics?.executionBlockProductionTimeSteps
: this.metrics?.builderBlockProductionTimeSteps;
const fork = currentState.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, currentState);
endAttestations?.({
step: BlockProductionStep.attestations,
});
const endEth1DataAndDeposits = stepsMetrics?.startTimer();
const { eth1Data, deposits } = await this.eth1.getEth1DataAndDeposits(currentState);
endEth1DataAndDeposits?.({
step: BlockProductionStep.eth1DataAndDeposits,
});
const blockBody = {
randaoReveal,
graffiti,
eth1Data,
proposerSlashings,
attesterSlashings,
attestations,
deposits,
voluntaryExits,
};
if (ForkSeq[fork] >= ForkSeq.capella) {
blockBody.blsToExecutionChanges = blsToExecutionChanges;
}
const endSyncAggregate = stepsMetrics?.startTimer();
if (ForkSeq[fork] >= ForkSeq.altair) {
const syncAggregate = this.syncContributionAndProofPool.getAggregate(parentSlot, parentBlockRoot);
this.metrics?.production.producedSyncAggregateParticipants.observe(syncAggregate.syncCommitteeBits.getTrueBitIndexes().length);
blockBody.syncAggregate = syncAggregate;
}
endSyncAggregate?.({
step: BlockProductionStep.syncAggregate,
});
return blockBody;
}
//# sourceMappingURL=produceBlockBody.js.map