diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43ceee898d85..deaf19329872 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -160,8 +160,8 @@ jobs: check-latest: true cache: yarn - # Remove when finished debugging core dumps - - uses: './.github/actions/setup-debug-node' + # # Remove when finished debugging core dumps + # - uses: './.github/actions/setup-debug-node' - name: Restore build cache id: cache-primes-restore @@ -184,13 +184,14 @@ jobs: key: spec-test-data-${{ hashFiles('packages/validator/test/spec/params.ts') }} - name: Unit tests - id: unit_tests + # id: unit_tests # Rever to "yarn test:unit" when finished debugging core dumps - run: sudo sh -c "ulimit -c unlimited && /usr/bin/node-with-debug $(which yarn) test:unit" + # run: sudo sh -c "ulimit -c unlimited && /usr/bin/node-with-debug $(which yarn) test:unit" + run: yarn test:unit - # Remove when finished debugging core dumps - - uses: './.github/actions/core-dump' - if: ${{ failure() && steps.unit_tests.conclusion == 'failure' }} + # # Remove when finished debugging core dumps + # - uses: './.github/actions/core-dump' + # if: ${{ failure() && steps.unit_tests.conclusion == 'failure' }} - name: Upload coverage data run: yarn coverage diff --git a/RELEASE.md b/RELEASE.md index 440b1a13fe82..b38a4f8562f6 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -30,7 +30,7 @@ To start a new release, one of the Lodestar developers will communicate this via - This script may alternatively be run on the checked out `HEAD`: - `git checkout 9fceb02` - `yarn release:create-rc 1.1.0` -- Open draft PR from `rc/v1.1.0` to `stable` with title `v1.1.0 release`. +- Open draft PR from `rc/v1.1.0` to `stable` with title `chore: v1.1.0 release`. #### Manual steps (for example version `v1.1.0`, commit `9fceb02`): @@ -42,7 +42,7 @@ To start a new release, one of the Lodestar developers will communicate this via - Commit changes - `git commit -am "v1.1.0"` - `git push origin rc/v1.1.0` -- Open draft PR from `rc/v1.1.0` to `stable` with title `v1.1.0 release`. +- Open draft PR from `rc/v1.1.0` to `stable` with title `chore: v1.1.0 release`. ### 2. Tag release candidate @@ -76,13 +76,13 @@ For example: After 3-5 days of testing, is performance equal to or better than l - **Yes**: Continue to the next release step - **No**: If it a small issue fixable quickly (hot-fix)? - **Yes**: Merge fix(es) to `unstable`, push the fix(es) to `rc/v1.1.0` branch, go to step 2, incrementing the rc version - - **No**: abort the release. Close the `v1.1.0 release` PR, delete the branch, and start the whole release process over. + - **No**: abort the release. Close the `chore: v1.1.0 release` PR, delete the branch, and start the whole release process over. ### 4. Merge release candidate - Ensure step 2 testing is successful and there is sufficient consensus to release `v1.1.0`. -- Approving the `v1.1.0 release` PR means a team member marks the release as safe, after personally reviewing and / or testing it. -- Merge `v1.1.0 release` PR to stable **with "merge commit"** strategy to preserve all history. +- Approving the `chore: v1.1.0 release` PR means a team member marks the release as safe, after personally reviewing and / or testing it. +- Merge `chore: v1.1.0 release` PR to stable **with "merge commit"** strategy to preserve all history. - Merge stable `stable` into `unstable` **with merge commit** strategy. Due to branch protections in `unstable` must open a PR. If there are conflicts, those must be resolved manually. Gitflow may cause changes that conflict between stable and unstable, for example due to a hotfix that is backported. If that happens, disable branch protections in unstable, merge locally fixing conflicts, run lint + tests, push, and re-enable branch protections. ### 5. Tag stable release @@ -130,7 +130,7 @@ A similar process for a stable release is used, with the three differences. - Switch to the hotfix release branch and cherrypick the inclusion(s) from the `unstable` branch to the hotfix release. - `git checkout rc/v1.1.1` - `git cherry-pick {commit}` -- Open draft PR from `rc/v1.1.1` to `stable` with the title `v1.1.1 release`. +- Open draft PR from `rc/v1.1.1` to `stable` with the title `chore: v1.1.1 release`. #### Manual steps (for example version `v1.1.1`, commit `8eb8dce`): @@ -144,7 +144,7 @@ A similar process for a stable release is used, with the three differences. - Commit changes - `git commit -am "v1.1.1"` - `git push origin rc/v1.1.1` - Open draft PR from `rc/v1.1.1` to `stable` with the title `v1.1.1 release`. + Open draft PR from `rc/v1.1.1` to `stable` with the title `chore: v1.1.1 release`. ### 2. Tag release candidate diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index ff785fc7b8bf..fc5cf953018d 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -50,7 +50,7 @@ import {RegenCaller} from "../../../chain/regen/index.js"; import {getValidatorStatus} from "../beacon/state/utils.js"; import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js"; import {SCHEDULER_LOOKAHEAD_FACTOR} from "../../../chain/prepareNextSlot.js"; -import {ChainEvent, CheckpointHex} from "../../../chain/index.js"; +import {ChainEvent, CheckpointHex, CommonBlockBody} from "../../../chain/index.js"; import {computeSubnetForCommitteesAtSlot, getPubkeysForIndices} from "./utils.js"; /** @@ -287,7 +287,11 @@ export function getValidatorApi({ // as of now fee recipient checks can not be performed because builder does not return bid recipient { skipHeadChecksAndUpdate, - }: Omit & {skipHeadChecksAndUpdate?: boolean} = {} + commonBlockBody, + }: Omit & { + skipHeadChecksAndUpdate?: boolean; + commonBlockBody?: CommonBlockBody; + } = {} ): Promise { const version = config.getForkName(slot); if (!isForkExecution(version)) { @@ -323,6 +327,7 @@ export function getValidatorApi({ slot, randaoReveal, graffiti: toGraffitiBuffer(graffiti || ""), + commonBlockBody, }); metrics?.blockProductionSuccess.inc({source}); @@ -352,7 +357,11 @@ export function getValidatorApi({ feeRecipient, strictFeeRecipientCheck, skipHeadChecksAndUpdate, - }: Omit & {skipHeadChecksAndUpdate?: boolean} = {} + commonBlockBody, + }: Omit & { + skipHeadChecksAndUpdate?: boolean; + commonBlockBody?: CommonBlockBody; + } = {} ): Promise { const source = ProducedBlockSource.engine; metrics?.blockProductionRequests.inc({source}); @@ -376,6 +385,7 @@ export function getValidatorApi({ randaoReveal, graffiti: toGraffitiBuffer(graffiti || ""), feeRecipient, + commonBlockBody, }); const version = config.getForkName(block.slot); if (strictFeeRecipientCheck && feeRecipient && isForkExecution(version)) { @@ -456,7 +466,7 @@ export function getValidatorApi({ chain.executionBuilder !== undefined && builderSelection !== routes.validator.BuilderSelection.ExecutionOnly; - logger.verbose("Assembling block with produceEngineOrBuilderBlock ", { + const loggerContext = { fork, builderSelection, slot, @@ -464,7 +474,16 @@ export function getValidatorApi({ strictFeeRecipientCheck, // winston logger doesn't like bigint builderBoostFactor: `${builderBoostFactor}`, + }; + + logger.verbose("Assembling block with produceEngineOrBuilderBlock", loggerContext); + const commonBlockBody = await chain.produceCommonBlockBody({ + slot, + randaoReveal, + graffiti: toGraffitiBuffer(graffiti || ""), }); + logger.debug("Produced common block body", loggerContext); + // Start calls for building execution and builder blocks const blindedBlockPromise = isBuilderEnabled ? // can't do fee recipient checks as builder bid doesn't return feeRecipient as of now @@ -472,6 +491,7 @@ export function getValidatorApi({ feeRecipient, // skip checking and recomputing head in these individual produce calls skipHeadChecksAndUpdate: true, + commonBlockBody, }).catch((e) => { logger.error("produceBuilderBlindedBlock failed to produce block", {slot}, e); return null; @@ -494,6 +514,7 @@ export function getValidatorApi({ strictFeeRecipientCheck, // skip checking and recomputing head in these individual produce calls skipHeadChecksAndUpdate: true, + commonBlockBody, }).catch((e) => { logger.error("produceEngineFullBlockOrContents failed to produce block", {slot}, e); return null; diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 4f96afe232ff..a42bc87c2749 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -43,7 +43,7 @@ import {ensureDir, writeIfNotExist} from "../util/file.js"; import {isOptimisticBlock} from "../util/forkChoice.js"; import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js"; import {ChainEventEmitter, ChainEvent} from "./emitter.js"; -import {IBeaconChain, ProposerPreparationData, BlockHash, StateGetOpts} from "./interface.js"; +import {IBeaconChain, ProposerPreparationData, BlockHash, StateGetOpts, CommonBlockBody} from "./interface.js"; import {IChainOptions} from "./options.js"; import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js"; import {initializeForkChoice} from "./forkChoice/index.js"; @@ -72,7 +72,7 @@ import {SeenBlockAttesters} from "./seenCache/seenBlockAttesters.js"; import {BeaconProposerCache} from "./beaconProposerCache.js"; import {CheckpointBalancesCache} from "./balancesCache.js"; import {AssembledBlockType, BlobsResultType, BlockType} from "./produceBlock/index.js"; -import {BlockAttributes, produceBlockBody} from "./produceBlock/produceBlockBody.js"; +import {BlockAttributes, produceBlockBody, produceCommonBlockBody} from "./produceBlock/produceBlockBody.js"; import {computeNewStateRoot} from "./produceBlock/computeNewStateRoot.js"; import {BlockInput} from "./blocks/types.js"; import {SeenAttestationDatas} from "./seenCache/seenAttestationData.js"; @@ -462,14 +462,35 @@ export class BeaconChain implements IBeaconChain { return {block: data, executionOptimistic: isOptimisticBlock(block)}; } // If block is not found in hot db, try cold db since there could be an archive cycle happening - // TODO: Add a lock to the archiver to have determinstic behaviour on where are blocks + // TODO: Add a lock to the archiver to have deterministic behavior on where are blocks } const data = await this.db.blockArchive.getByRoot(fromHexString(root)); return data && {block: data, executionOptimistic: false}; } - produceBlock(blockAttributes: BlockAttributes): Promise<{ + async produceCommonBlockBody(blockAttributes: BlockAttributes): Promise { + const {slot} = blockAttributes; + const head = this.forkChoice.getHead(); + const state = await this.regen.getBlockSlotState( + head.blockRoot, + slot, + {dontTransferCache: true}, + RegenCaller.produceBlock + ); + const parentBlockRoot = fromHexString(head.blockRoot); + + // TODO: To avoid breaking changes for metric define this attribute + const blockType = BlockType.Full; + + return produceCommonBlockBody.call(this, blockType, state, { + ...blockAttributes, + parentBlockRoot, + parentSlot: slot - 1, + }); + } + + produceBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{ block: allForks.BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Wei; @@ -478,7 +499,7 @@ export class BeaconChain implements IBeaconChain { return this.produceBlockWrapper(BlockType.Full, blockAttributes); } - produceBlindedBlock(blockAttributes: BlockAttributes): Promise<{ + produceBlindedBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{ block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Wei; @@ -488,7 +509,7 @@ export class BeaconChain implements IBeaconChain { async produceBlockWrapper( blockType: T, - {randaoReveal, graffiti, slot, feeRecipient}: BlockAttributes + {randaoReveal, graffiti, slot, feeRecipient, commonBlockBody}: BlockAttributes & {commonBlockBody?: CommonBlockBody} ): Promise<{ block: AssembledBlockType; executionPayloadValue: Wei; @@ -519,6 +540,7 @@ export class BeaconChain implements IBeaconChain { parentBlockRoot, proposerIndex, proposerPubKey, + commonBlockBody, } ); diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index da364c9ea447..51eb72b6a6dd 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -1,5 +1,18 @@ import {CompositeTypeAny, TreeView, Type} from "@chainsafe/ssz"; -import {allForks, UintNum64, Root, phase0, Slot, RootHex, Epoch, ValidatorIndex, deneb, Wei} from "@lodestar/types"; +import { + allForks, + UintNum64, + Root, + phase0, + Slot, + RootHex, + Epoch, + ValidatorIndex, + deneb, + Wei, + capella, + altair, +} from "@lodestar/types"; import { BeaconStateAllForks, CachedBeaconStateAllForks, @@ -142,13 +155,14 @@ export interface IBeaconChain { getContents(beaconBlock: deneb.BeaconBlock): deneb.Contents; - produceBlock(blockAttributes: BlockAttributes): Promise<{ + produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; + produceBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{ block: allForks.BeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Wei; shouldOverrideBuilder?: boolean; }>; - produceBlindedBlock(blockAttributes: BlockAttributes): Promise<{ + produceBlindedBlock(blockAttributes: BlockAttributes & {commonBlockBody?: CommonBlockBody}): Promise<{ block: allForks.BlindedBeaconBlock; executionPayloadValue: Wei; consensusBlockValue: Wei; @@ -192,3 +206,7 @@ export type SSZObjectType = | "signedAggregatedAndProof" | "syncCommittee" | "contributionAndProof"; + +export type CommonBlockBody = phase0.BeaconBlockBody & + Pick & + Pick; diff --git a/packages/beacon-node/src/chain/opPools/opPool.ts b/packages/beacon-node/src/chain/opPools/opPool.ts index 00a2c4e3cc96..1fdee886ff1d 100644 --- a/packages/beacon-node/src/chain/opPools/opPool.ts +++ b/packages/beacon-node/src/chain/opPools/opPool.ts @@ -180,7 +180,7 @@ export class OpPool { ] { const {config} = state; const stateEpoch = computeEpochAtSlot(state.slot); - const stateFork = config.getForkName(state.slot); + const stateFork = config.getForkSeq(state.slot); const toBeSlashedIndices = new Set(); const proposerSlashings: phase0.ProposerSlashing[] = []; @@ -249,7 +249,10 @@ export class OpPool { // Signature validation is skipped in `isValidVoluntaryExit(,,false)` since it was already validated in gossip // However we must make sure that the signature fork is the same, or it will become invalid if included through // a future fork. - stateFork === config.getForkName(computeStartSlotAtEpoch(voluntaryExit.message.epoch)) + isVoluntaryExitSignatureIncludable( + stateFork, + config.getForkSeq(computeStartSlotAtEpoch(voluntaryExit.message.epoch)) + ) ) { voluntaryExits.push(voluntaryExit); if (voluntaryExits.length >= MAX_VOLUNTARY_EXITS) { @@ -400,6 +403,19 @@ export class OpPool { } } +/** + * Returns true if a pre-validated signature is still valid to be included in a specific block's fork + */ +function isVoluntaryExitSignatureIncludable(stateFork: ForkSeq, voluntaryExitFork: ForkSeq): boolean { + if (stateFork >= ForkSeq.deneb) { + // Exists are perpetually valid https://eips.ethereum.org/EIPS/eip-7044 + return true; + } else { + // Can only include exits from the current and previous fork + return voluntaryExitFork === stateFork || voluntaryExitFork === stateFork - 1; + } +} + function isSlashableAtEpoch(validator: phase0.Validator, epoch: Epoch): boolean { return !validator.slashed && validator.activationEpoch <= epoch && epoch < validator.withdrawableEpoch; } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 0b6ff7b1316b..b25b71514a71 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -1,8 +1,6 @@ import { Bytes32, - phase0, allForks, - altair, Root, RootHex, Slot, @@ -35,6 +33,7 @@ import {PayloadId, IExecutionEngine, IExecutionBuilder, PayloadAttributes} from import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js"; import {IEth1ForBlockProduction} from "../../eth1/index.js"; import {numToQuantity} from "../../eth1/provider/utils.js"; +import {CommonBlockBody} from "../interface.js"; import {validateBlobsAndKzgCommitments} from "./validateBlobsAndKzgCommitments.js"; // Time to provide the EL to generate a payload from new payload id @@ -94,20 +93,12 @@ export async function produceBlockBody( this: BeaconChain, blockType: T, currentState: CachedBeaconStateAllForks, - { - randaoReveal, - graffiti, - slot: blockSlot, - feeRecipient: requestedFeeRecipient, - parentSlot, - parentBlockRoot, - proposerIndex, - proposerPubKey, - }: BlockAttributes & { + blockAttr: BlockAttributes & { parentSlot: Slot; parentBlockRoot: Root; proposerIndex: ValidatorIndex; proposerPubKey: BLSPubkey; + commonBlockBody?: CommonBlockBody; } ): Promise<{ body: AssembledBodyType; @@ -115,6 +106,14 @@ export async function produceBlockBody( executionPayloadValue: Wei; shouldOverrideBuilder?: boolean; }> { + const { + slot: blockSlot, + feeRecipient: requestedFeeRecipient, + parentBlockRoot, + proposerIndex, + proposerPubKey, + commonBlockBody, + } = 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 @@ -131,63 +130,17 @@ export async function produceBlockBody( slot: blockSlot, }; this.logger.verbose("Producing beacon block body", logMeta); - - // 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 stepsMetrics = blockType === BlockType.Full ? this.metrics?.executionBlockProductionTimeSteps : this.metrics?.builderBlockProductionTimeSteps; - const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = - this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); - - const endAttestations = stepsMetrics?.startTimer(); - const attestations = this.aggregatedAttestationPool.getAttestationsForBlock(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 = commonBlockBody + ? Object.assign({}, commonBlockBody) + : await produceCommonBlockBody.call(this, blockType, currentState, blockAttr); - const blockBody: phase0.BeaconBlockBody = { - randaoReveal, - graffiti, - eth1Data, - proposerSlashings, - attesterSlashings, - attestations, - deposits, - voluntaryExits, - }; - - const blockEpoch = computeEpochAtSlot(blockSlot); - - const endSyncAggregate = stepsMetrics?.startTimer(); - if (blockEpoch >= this.config.ALTAIR_FORK_EPOCH) { - const syncAggregate = this.syncContributionAndProofPool.getAggregate(parentSlot, parentBlockRoot); - this.metrics?.production.producedSyncAggregateParticipants.observe( - syncAggregate.syncCommitteeBits.getTrueBitIndexes().length - ); - (blockBody as altair.BeaconBlockBody).syncAggregate = syncAggregate; - } - endSyncAggregate?.({ - step: BlockProductionStep.syncAggregate, - }); + const {attestations, deposits, voluntaryExits, attesterSlashings, proposerSlashings, blsToExecutionChanges} = + blockBody; Object.assign(logMeta, { attestations: attestations.length, @@ -317,6 +270,7 @@ export async function produceBlockBody( prepType, payloadId, fetchedTime, + executionHeadBlockHash: toHex(engineRes.executionPayload.blockHash), }); if (executionPayload.transactions.length === 0) { this.metrics?.blockPayload.emptyPayloads.inc({prepType}); @@ -373,8 +327,6 @@ export async function produceBlockBody( }); if (ForkSeq[fork] >= ForkSeq.capella) { - // TODO: blsToExecutionChanges should be passed in the produceBlock call - (blockBody as capella.BeaconBlockBody).blsToExecutionChanges = blsToExecutionChanges; Object.assign(logMeta, { blsToExecutionChanges: blsToExecutionChanges.length, }); @@ -616,4 +568,81 @@ function preparePayloadAttributes( return payloadAttributes; } -/** process_sync_committee_contributions is implemented in syncCommitteeContribution.getSyncAggregate */ +export async function produceCommonBlockBody( + this: BeaconChain, + blockType: T, + currentState: CachedBeaconStateAllForks, + { + randaoReveal, + graffiti, + slot, + parentSlot, + parentBlockRoot, + }: BlockAttributes & { + parentSlot: Slot; + parentBlockRoot: Root; + } +): Promise { + const stepsMetrics = + blockType === BlockType.Full + ? this.metrics?.executionBlockProductionTimeSteps + : this.metrics?.builderBlockProductionTimeSteps; + + const blockEpoch = computeEpochAtSlot(slot); + 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(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: Omit = { + randaoReveal, + graffiti, + eth1Data, + proposerSlashings, + attesterSlashings, + attestations, + deposits, + voluntaryExits, + }; + + if (ForkSeq[fork] >= ForkSeq.capella) { + (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges; + } + + const endSyncAggregate = stepsMetrics?.startTimer(); + if (blockEpoch >= this.config.ALTAIR_FORK_EPOCH) { + const syncAggregate = this.syncContributionAndProofPool.getAggregate(parentSlot, parentBlockRoot); + this.metrics?.production.producedSyncAggregateParticipants.observe( + syncAggregate.syncCommitteeBits.getTrueBitIndexes().length + ); + (blockBody as CommonBlockBody).syncAggregate = syncAggregate; + } + endSyncAggregate?.({ + step: BlockProductionStep.syncAggregate, + }); + + return blockBody as CommonBlockBody; +} diff --git a/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts b/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts index 3c5dacc9c971..c72d22471ce8 100644 --- a/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts +++ b/packages/beacon-node/test/__mocks__/mockedBeaconChain.ts @@ -79,6 +79,7 @@ vi.mock("../../src/chain/index.js", async (requireActual) => { // @ts-expect-error beaconProposerCache: new BeaconProposerCache(), shufflingCache: new ShufflingCache(), + produceCommonBlockBody: vi.fn(), produceBlock: vi.fn(), produceBlindedBlock: vi.fn(), getCanonicalBlockAtSlot: vi.fn(), diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts index 3a87b709b741..f1aa2cb791df 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV3.test.ts @@ -9,6 +9,7 @@ import {getValidatorApi} from "../../../../../src/api/impl/validator/index.js"; import {testLogger} from "../../../../utils/logger.js"; import {ApiImplTestModules, setupApiImplTestServer} from "../../../../__mocks__/apiMocks.js"; import {ExecutionBuilderHttp} from "../../../../../src/execution/builder/http.js"; +import {CommonBlockBody} from "../../../../../src/chain/interface.js"; /* eslint-disable @typescript-eslint/naming-convention */ describe("api/validator - produceBlockV3", function () { @@ -100,6 +101,21 @@ describe("api/validator - produceBlockV3", function () { const api = getValidatorApi(modules); if (enginePayloadValue !== null) { + const commonBlockBody: CommonBlockBody = { + attestations: fullBlock.body.attestations, + attesterSlashings: fullBlock.body.attesterSlashings, + deposits: fullBlock.body.deposits, + proposerSlashings: fullBlock.body.proposerSlashings, + eth1Data: fullBlock.body.eth1Data, + graffiti: fullBlock.body.graffiti, + randaoReveal: fullBlock.body.randaoReveal, + voluntaryExits: fullBlock.body.voluntaryExits, + blsToExecutionChanges: [], + syncAggregate: fullBlock.body.syncAggregate, + }; + + chainStub.produceCommonBlockBody.mockResolvedValue(commonBlockBody); + chainStub.produceBlock.mockResolvedValue({ block: fullBlock, executionPayloadValue: BigInt(enginePayloadValue), diff --git a/packages/cli/src/cmds/validator/handler.ts b/packages/cli/src/cmds/validator/handler.ts index 57b00d78c706..5eca9bc741d9 100644 --- a/packages/cli/src/cmds/validator/handler.ts +++ b/packages/cli/src/cmds/validator/handler.ts @@ -227,7 +227,7 @@ function getProposerConfigFromArgs( selection: parseBuilderSelection( args["builder.selection"] ?? (args["builder"] ? defaultOptions.builderAliasSelection : undefined) ), - boostFactor: args["builder.boostFactor"], + boostFactor: args["builder.boostFactor"] !== undefined ? BigInt(args["builder.boostFactor"]) : undefined, }, }; diff --git a/packages/cli/src/cmds/validator/options.ts b/packages/cli/src/cmds/validator/options.ts index e68e04a4b884..cddb34981ce1 100644 --- a/packages/cli/src/cmds/validator/options.ts +++ b/packages/cli/src/cmds/validator/options.ts @@ -248,7 +248,7 @@ export const validatorOptions: CliCommandOptions = { }, "builder.boostFactor": { - type: "number", + type: "string", description: "Percentage multiplier the block producing beacon node must apply to boost (>100) or dampen (<100) builder block value for selection against execution block. The multiplier is ignored if `--builder.selection` is set to anything other than `maxprofit`", defaultDescription: `${defaultOptions.builderBoostFactor}`, @@ -257,8 +257,7 @@ export const validatorOptions: CliCommandOptions = { useProduceBlockV3: { type: "boolean", - description: "Enable/disable usage of produceBlockV3 that might not be supported by all beacon clients yet", - defaultDescription: `${defaultOptions.useProduceBlockV3}`, + description: "Enable/disable usage of produceBlockV3 for block production, is auto enabled on deneb+ blocks", }, broadcastValidation: { diff --git a/packages/cli/src/networks/goerli.ts b/packages/cli/src/networks/goerli.ts index b076562398a3..c58dcabae6a0 100644 --- a/packages/cli/src/networks/goerli.ts +++ b/packages/cli/src/networks/goerli.ts @@ -1,10 +1,8 @@ export {goerliChainConfig as chainConfig} from "@lodestar/config/networks"; export const depositContractDeployBlock = 4367322; -export const genesisFileUrl = - "https://raw.githubusercontent.com/eth-clients/eth2-networks/master/shared/prater/genesis.ssz"; -export const bootnodesFileUrl = - "https://raw.githubusercontent.com/eth-clients/eth2-networks/master/shared/prater/bootstrap_nodes.txt"; +export const genesisFileUrl = "https://raw.githubusercontent.com/eth-clients/goerli/main/prater/genesis.ssz"; +export const bootnodesFileUrl = "https://raw.githubusercontent.com/eth-clients/goerli/main/prater/bootstrap_nodes.txt"; export const bootEnrs = [ "enr:-LK4QH1xnjotgXwg25IDPjrqRGFnH1ScgNHA3dv1Z8xHCp4uP3N3Jjl_aYv_WIxQRdwZvSukzbwspXZ7JjpldyeVDzMCh2F0dG5ldHOIAAAAAAAAAACEZXRoMpB53wQoAAAQIP__________gmlkgnY0gmlwhIe1te-Jc2VjcDI1NmsxoQOkcGXqbCJYbcClZ3z5f6NWhX_1YPFRYRRWQpJjwSHpVIN0Y3CCIyiDdWRwgiMo", diff --git a/packages/cli/test/utils/simulation/validator_clients/lodestar.ts b/packages/cli/test/utils/simulation/validator_clients/lodestar.ts index 7c0c9e3537b1..f7a1e808a778 100644 --- a/packages/cli/test/utils/simulation/validator_clients/lodestar.ts +++ b/packages/cli/test/utils/simulation/validator_clients/lodestar.ts @@ -39,7 +39,7 @@ export const generateLodestarValidatorNode: ValidatorNodeGenerator}; type BlockProposalOpts = { - useProduceBlockV3: boolean; + useProduceBlockV3?: boolean; broadcastValidation: routes.beacon.BroadcastValidation; blindedLocal: boolean; }; @@ -125,6 +125,7 @@ export class BlockProposingService { this.validatorStore.getBuilderSelectionParams(pubkeyHex); const feeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex); const blindedLocal = this.opts.blindedLocal; + const useProduceBlockV3 = this.opts.useProduceBlockV3 ?? this.config.getForkSeq(slot) >= ForkSeq.deneb; this.logger.debug("Producing block", { ...debugLogCtx, @@ -132,12 +133,12 @@ export class BlockProposingService { builderBoostFactor, feeRecipient, strictFeeRecipientCheck, - useProduceBlockV3: this.opts.useProduceBlockV3, + useProduceBlockV3, blindedLocal, }); this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot)); - const produceBlockFn = this.opts.useProduceBlockV3 ? this.produceBlockWrapper : this.produceBlockV2Wrapper; + const produceBlockFn = useProduceBlockV3 ? this.produceBlockWrapper : this.produceBlockV2Wrapper; const produceOpts = { feeRecipient, strictFeeRecipientCheck, diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index 809ca0c8a7c6..03811062c2ad 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -126,15 +126,13 @@ export const defaultOptions = { builderSelection: routes.validator.BuilderSelection.ExecutionOnly, builderAliasSelection: routes.validator.BuilderSelection.MaxProfit, builderBoostFactor: BigInt(100), - // turn it off by default, turn it back on once other clients support v3 api - useProduceBlockV3: false, // spec asks for gossip validation by default broadcastValidation: routes.beacon.BroadcastValidation.gossip, // should request fetching the locally produced block in blinded format blindedLocal: false, }; -export const MAX_BUILDER_BOOST_FACTOR = BigInt(2 ** 64 - 1); +export const MAX_BUILDER_BOOST_FACTOR = 2n ** 64n - 1n; /** * Service that sets up and handles validator attester duties. diff --git a/packages/validator/src/validator.ts b/packages/validator/src/validator.ts index f28a9afbaff6..deff99ee93f0 100644 --- a/packages/validator/src/validator.ts +++ b/packages/validator/src/validator.ts @@ -209,7 +209,7 @@ export class Validator { const chainHeaderTracker = new ChainHeaderTracker(logger, api, emitter); const blockProposingService = new BlockProposingService(config, loggerVc, api, clock, validatorStore, metrics, { - useProduceBlockV3: opts.useProduceBlockV3 ?? defaultOptions.useProduceBlockV3, + useProduceBlockV3: opts.useProduceBlockV3, broadcastValidation: opts.broadcastValidation ?? defaultOptions.broadcastValidation, blindedLocal: opts.blindedLocal ?? defaultOptions.blindedLocal, }); @@ -289,18 +289,15 @@ export class Validator { await assertEqualGenesis(opts, genesis); logger.info("Verified connected beacon node and validator have the same genesisValidatorRoot"); - const { - useProduceBlockV3 = defaultOptions.useProduceBlockV3, - broadcastValidation = defaultOptions.broadcastValidation, - valProposerConfig, - } = opts; + const {useProduceBlockV3, broadcastValidation = defaultOptions.broadcastValidation, valProposerConfig} = opts; const defaultBuilderSelection = valProposerConfig?.defaultConfig.builder?.selection ?? defaultOptions.builderSelection; const strictFeeRecipientCheck = valProposerConfig?.defaultConfig.strictFeeRecipientCheck ?? false; const suggestedFeeRecipient = valProposerConfig?.defaultConfig.feeRecipient ?? defaultOptions.suggestedFeeRecipient; logger.info("Initializing validator", { - useProduceBlockV3, + // if no explicit option is provided, useProduceBlockV3 will be auto enabled on/post deneb + useProduceBlockV3: useProduceBlockV3 === undefined ? "deneb+" : useProduceBlockV3, broadcastValidation, defaultBuilderSelection, suggestedFeeRecipient,