@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
593 lines • 31.8 kB
JavaScript
import { getSafeExecutionBlockHash } from "@lodestar/fork-choice";
import { BUILDER_INDEX_SELF_BUILD, ForkSeq, isForkPostAltair, isForkPostBellatrix, isForkPostGloas, } from "@lodestar/params";
import { G2_POINT_AT_INFINITY, computeTimeAtSlot, isStatePostBellatrix, isStatePostCapella, isStatePostGloas, } from "@lodestar/state-transition";
import { ssz, } from "@lodestar/types";
import { 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 { getExpectedGasLimit, } from "../../execution/index.js";
import { fromGraffitiBytes } from "../../util/graffiti.js";
import { kzg } from "../../util/kzg.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 { PayloadPreparationType };
var PayloadPreparationType;
(function (PayloadPreparationType) {
PayloadPreparationType["Fresh"] = "Fresh";
PayloadPreparationType["Cached"] = "Cached";
PayloadPreparationType["Reorged"] = "Reorged";
PayloadPreparationType["Blinded"] = "Blinded";
})(PayloadPreparationType || (PayloadPreparationType = {}));
export { BlockProductionStep };
/**
* Block production steps tracked in metrics
*/
var BlockProductionStep;
(function (BlockProductionStep) {
BlockProductionStep["proposerSlashing"] = "proposerSlashing";
BlockProductionStep["attesterSlashings"] = "attesterSlashings";
BlockProductionStep["voluntaryExits"] = "voluntaryExits";
BlockProductionStep["blsToExecutionChanges"] = "blsToExecutionChanges";
BlockProductionStep["attestations"] = "attestations";
BlockProductionStep["syncAggregate"] = "syncAggregate";
BlockProductionStep["executionPayload"] = "executionPayload";
})(BlockProductionStep || (BlockProductionStep = {}));
export { BlockType };
var BlockType;
(function (BlockType) {
BlockType["Full"] = "Full";
BlockType["Blinded"] = "Blinded";
})(BlockType || (BlockType = {}));
export async function produceBlockBody(blockType, currentState, blockAttr) {
const { slot: blockSlot, feeRecipient: requestedFeeRecipient, parentBlock, proposerIndex, proposerPubKey, commonBlockBodyPromise, } = blockAttr;
let executionPayloadValue;
let blockBody;
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;
const fork = this.config.getForkName(blockSlot);
const produceResult = {
type: blockType,
fork,
};
const logMeta = {
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;
let parentExecutionRequests;
// Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below
let stateAfterParentPayload = 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 = {
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 = {
message: bid,
signature: G2_POINT_AT_INFINITY,
};
const commonBlockBody = await commonBlockBodyPromise;
const gloasBody = Object.assign({}, commonBlockBody);
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;
// Store execution payload data required to construct execution payload envelope later
const gloasResult = produceResult;
gloasResult.executionPayload = executionPayload;
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);
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;
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.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);
{
const { prepType, payloadId, executionPayload, blobsBundle, executionRequests } = engineRes;
shouldOverrideBuilder = engineRes.shouldOverrideBuilder;
blockBody.executionPayload = executionPayload;
produceResult.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.blobKzgCommitments = blobsBundle.commitments;
produceResult.blobsBundle = blobsBundle;
produceResult.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.blobKzgCommitments = blobsBundle.commitments;
produceResult.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.executionRequests = executionRequests;
}
}
}
}
else {
const commonBlockBody = await commonBlockBodyPromise;
blockBody = Object.assign({}, commonBlockBody);
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.gloas) {
const { blsToExecutionChanges, payloadAttestations } = blockBody;
Object.assign(logMeta, {
blsToExecutionChanges: blsToExecutionChanges.length,
payloadAttestations: payloadAttestations.length,
});
}
else 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, produceResult, executionPayloadValue, shouldOverrideBuilder };
}
/**
* Produce ExecutionPayload for post-merge.
*/
export async function prepareExecutionPayload(chain, logger, fork, parentBlockRoot, parentBlockHash, safeBlockHash, finalizedBlockHash,
/**
* Post-gloas, when extending a full parent, callers must apply
* parent execution payload first (see `withParentPayloadApplied`).
*/
state, suggestedFeeRecipient) {
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;
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,
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, fork, state, proposerPubKey) {
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, chain, { prepareState, prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient, }) {
const payloadAttributes = preparePayloadAttributes(fork, chain, {
prepareState,
prepareSlot,
parentBlockRoot,
parentBlockHash,
feeRecipient,
});
let parentBlockNumber;
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 = {
proposerIndex: prepareState.getBeaconProposer(prepareSlot),
proposalSlot: prepareSlot,
parentBlockNumber,
parentBlockRoot,
parentBlockHash,
payloadAttributes,
};
return ssePayloadAttributes;
}
function preparePayloadAttributes(fork, chain, { prepareState, prepareSlot, parentBlockRoot, parentBlockHash, feeRecipient, }) {
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.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.withdrawals =
prepareState.payloadExpectedWithdrawals;
}
}
else {
// withdrawals logic is now fork aware as it changes on electra fork post capella
payloadAttributes.withdrawals =
prepareState.getExpectedWithdrawals().expectedWithdrawals;
}
}
if (ForkSeq[fork] >= ForkSeq.deneb) {
payloadAttributes.parentBeaconBlockRoot = parentBlockRoot;
}
if (ForkSeq[fork] >= ForkSeq.gloas) {
payloadAttributes.slotNumber = prepareSlot;
}
return payloadAttributes;
}
export async function produceCommonBlockBody(blockType, currentState, { randaoReveal, graffiti, slot, parentBlock }) {
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 = {
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.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.syncAggregate = syncAggregate;
}
endSyncAggregate?.({
step: BlockProductionStep.syncAggregate,
});
return blockBody;
}
//# sourceMappingURL=produceBlockBody.js.map