diff --git a/packages/beacon-state-transition/src/cache/epochContext.ts b/packages/beacon-state-transition/src/cache/epochContext.ts index d0d121fa55ef..78e615f0a9ba 100644 --- a/packages/beacon-state-transition/src/cache/epochContext.ts +++ b/packages/beacon-state-transition/src/cache/epochContext.ts @@ -103,6 +103,17 @@ export class EpochContext { * 32 x Number */ proposers: ValidatorIndex[]; + + /** + * Indexes of the block proposers for the next current epoch. + * + * We allow requesting proposal duties only one epoch in the future + * Note: There is a small probability that returned validators differs + * than what is returned when the epoch is reached. + * + * 32 x Number + */ + nextEpochProposers: ValidatorIndex[]; /** * Shuffling of validator indexes. Immutable through the epoch, then it's replaced entirely. * Note: Per spec definition, shuffling will always be defined. They are never called before loadState() @@ -170,6 +181,7 @@ export class EpochContext { pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; proposers: number[]; + nextEpochProposers: number[]; previousShuffling: IEpochShuffling; currentShuffling: IEpochShuffling; nextShuffling: IEpochShuffling; @@ -190,6 +202,7 @@ export class EpochContext { this.pubkey2index = data.pubkey2index; this.index2pubkey = data.index2pubkey; this.proposers = data.proposers; + this.nextEpochProposers = data.nextEpochProposers; this.previousShuffling = data.previousShuffling; this.currentShuffling = data.currentShuffling; this.nextShuffling = data.nextShuffling; @@ -283,6 +296,8 @@ export class EpochContext { // Allow to create CachedBeaconState for empty states const proposers = state.validators.length > 0 ? computeProposers(state, currentShuffling, effectiveBalanceIncrements) : []; + const nextEpochProposers = + state.validators.length > 0 ? computeProposers(state, nextShuffling, effectiveBalanceIncrements) : []; // Only after altair, compute the indices of the current sync committee const afterAltairFork = currentEpoch >= config.ALTAIR_FORK_EPOCH; @@ -330,6 +345,7 @@ export class EpochContext { pubkey2index, index2pubkey, proposers, + nextEpochProposers, previousShuffling, currentShuffling, nextShuffling, @@ -362,6 +378,7 @@ export class EpochContext { index2pubkey: this.index2pubkey, // Immutable data proposers: this.proposers, + nextEpochProposers: this.nextEpochProposers, previousShuffling: this.previousShuffling, currentShuffling: this.currentShuffling, nextShuffling: this.nextShuffling, @@ -400,6 +417,7 @@ export class EpochContext { const nextEpoch = currEpoch + 1; this.nextShuffling = computeEpochShuffling(state, epochProcess.nextEpochShufflingActiveValidatorIndices, nextEpoch); this.proposers = computeProposers(state, this.currentShuffling, this.effectiveBalanceIncrements); + this.nextEpochProposers = computeProposers(state, this.nextShuffling, this.effectiveBalanceIncrements); // TODO: DEDUPLICATE from createEpochContext // @@ -477,6 +495,10 @@ export class EpochContext { return this.proposers[slot % SLOTS_PER_EPOCH]; } + async getNextEpochBeaconProposer(): Promise { + return this.nextEpochProposers; + } + /** * Return the indexed attestation corresponding to ``attestation``. */ diff --git a/packages/lodestar/src/api/impl/validator/index.ts b/packages/lodestar/src/api/impl/validator/index.ts index 87afb65a2ccd..e877c46c8ba7 100644 --- a/packages/lodestar/src/api/impl/validator/index.ts +++ b/packages/lodestar/src/api/impl/validator/index.ts @@ -286,7 +286,7 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}: // Note: There is a small probability that returned validators differs // than what is returned when the epoch is reached. if (epoch === nextEpoch) { - indexes.push(...(await chain.getNextEpochProposerDuty())); + indexes.push(...(await state.getNextEpochBeaconProposer())); } else { // Gather indexes to get pubkeys in batch (performance optimization) for (let i = 0; i < SLOTS_PER_EPOCH; i++) { @@ -295,13 +295,6 @@ export function getValidatorApi({chain, config, logger, metrics, network, sync}: const validatorIndex = state.getBeaconProposer(startSlot + i); indexes.push(validatorIndex); } - - for (const cachedEpoch of chain.nextEpochProposerDutyCache.keys()) { - // Do not keep past cached future proposal duties. - if (cachedEpoch <= epoch) { - chain.nextEpochProposerDutyCache.delete(cachedEpoch); - } - } } // NOTE: this is the fastest way of getting compressed pubkeys. diff --git a/packages/lodestar/src/chain/chain.ts b/packages/lodestar/src/chain/chain.ts index e21013880e0c..92f27ce2d5d1 100644 --- a/packages/lodestar/src/chain/chain.ts +++ b/packages/lodestar/src/chain/chain.ts @@ -3,14 +3,10 @@ */ import fs from "node:fs"; -import { - CachedBeaconStateAllForks, - computeProposers, - computeStartSlotAtEpoch, -} from "@chainsafe/lodestar-beacon-state-transition"; +import {CachedBeaconStateAllForks, computeStartSlotAtEpoch} from "@chainsafe/lodestar-beacon-state-transition"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; -import {allForks, Number64, Root, phase0, Slot, RootHex, ValidatorIndex, Epoch} from "@chainsafe/lodestar-types"; +import {allForks, Number64, Root, phase0, Slot, RootHex} from "@chainsafe/lodestar-types"; import {ILogger} from "@chainsafe/lodestar-utils"; import {fromHexString, TreeBacked} from "@chainsafe/ssz"; import {AbortController} from "@chainsafe/abort-controller"; @@ -82,9 +78,6 @@ export class BeaconChain implements IBeaconChain { readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); readonly seenContributionAndProof = new SeenContributionAndProof(); - // caches computed proposers for epoch N+1 in the future - readonly nextEpochProposerDutyCache = new Map(); - protected readonly blockProcessor: BlockProcessor; protected readonly db: IBeaconDb; protected readonly logger: ILogger; @@ -226,28 +219,6 @@ export class BeaconChain implements IBeaconChain { return headState; } - async getNextEpochProposerDuty(): Promise { - const nextEpoch = this.clock.currentEpoch + 1; - const cachedDutiesForEpoch = this.nextEpochProposerDutyCache.get(nextEpoch); - - if (cachedDutiesForEpoch) { - return cachedDutiesForEpoch; - } else { - const state = await this.getHeadStateAtCurrentEpoch(); - const futureProposers = computeProposers( - state, - state.getShufflingAtEpoch(nextEpoch), - state.effectiveBalanceIncrements - ); - - // Do not keep previous future proposal duties, so clear cache - // before setting a new value. - this.nextEpochProposerDutyCache.clear(); - this.nextEpochProposerDutyCache.set(nextEpoch, futureProposers); - return futureProposers; - } - } - async getHeadStateAtCurrentEpoch(): Promise { const currentEpochStartSlot = computeStartSlotAtEpoch(this.clock.currentEpoch); const head = this.forkChoice.getHead(); diff --git a/packages/lodestar/src/chain/interface.ts b/packages/lodestar/src/chain/interface.ts index dc2773505d3a..e16d6d527bd0 100644 --- a/packages/lodestar/src/chain/interface.ts +++ b/packages/lodestar/src/chain/interface.ts @@ -1,4 +1,4 @@ -import {allForks, Number64, Root, phase0, Slot, RootHex, ValidatorIndex, Epoch} from "@chainsafe/lodestar-types"; +import {allForks, Number64, Root, phase0, Slot, RootHex} from "@chainsafe/lodestar-types"; import {CachedBeaconStateAllForks} from "@chainsafe/lodestar-beacon-state-transition"; import {IForkChoice} from "@chainsafe/lodestar-fork-choice"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; @@ -68,9 +68,6 @@ export interface IBeaconChain { readonly seenSyncCommitteeMessages: SeenSyncCommitteeMessages; readonly seenContributionAndProof: SeenContributionAndProof; - // caches computed proposers for epoch N+1 - readonly nextEpochProposerDutyCache: Map; - /** Stop beacon chain processing */ close(): void; /** Populate in-memory caches with persisted data. Call at least once on startup */ @@ -82,11 +79,6 @@ export interface IBeaconChain { getHeadState(): CachedBeaconStateAllForks; getHeadStateAtCurrentEpoch(): Promise; - /** We allow requesting proposal duties only one epoch in the future - Note: There is a small probability that returned validators differs - than what is returned when the epoch is reached.**/ - getNextEpochProposerDuty(): Promise; - /** * Since we can have multiple parallel chains, * this methods returns blocks in current chain head according to diff --git a/packages/lodestar/test/unit/api/impl/validator/duties/proposer.test.ts b/packages/lodestar/test/unit/api/impl/validator/duties/proposer.test.ts index 7def0a5045f9..0da5d43844c9 100644 --- a/packages/lodestar/test/unit/api/impl/validator/duties/proposer.test.ts +++ b/packages/lodestar/test/unit/api/impl/validator/duties/proposer.test.ts @@ -100,7 +100,6 @@ describe("get proposers api impl", function () { ); const cachedState = createCachedBeaconState(config, state); chainStub.getHeadStateAtCurrentEpoch.resolves(cachedState); - chainStub.getNextEpochProposerDuty.resolves(new Array(SLOTS_PER_EPOCH)); const stubGetBeaconProposer = sinon.stub(cachedState.epochCtx, "getBeaconProposer"); stubGetBeaconProposer.returns(1); const {data: result} = await api.getProposerDuties(1); @@ -108,6 +107,32 @@ describe("get proposers api impl", function () { expect(stubGetBeaconProposer.called, "stubGetBeaconProposer function should not have been called").to.be.false; }); + it("should have different proposer for current and next epoch", async function () { + syncStub.isSynced.returns(true); + server.sandbox.stub(chainStub.clock, "currentEpoch").get(() => 0); + server.sandbox.stub(chainStub.clock, "currentSlot").get(() => 0); + dbStub.block.get.resolves({message: {stateRoot: Buffer.alloc(32)}} as any); + const state = generateState( + { + slot: 0, + validators: generateValidators(25, { + effectiveBalance: MAX_EFFECTIVE_BALANCE, + activationEpoch: 0, + exitEpoch: FAR_FUTURE_EPOCH, + }), + balances: generateInitialMaxBalances(config, 25), + }, + config + ); + const cachedState = createCachedBeaconState(config, state); + chainStub.getHeadStateAtCurrentEpoch.resolves(cachedState); + const stubGetBeaconProposer = sinon.stub(cachedState.epochCtx, "getBeaconProposer"); + stubGetBeaconProposer.returns(1); + const {data: currentProposers} = await api.getProposerDuties(0); + const {data: nextProposers} = await api.getProposerDuties(1); + expect(currentProposers).to.not.deep.equal(nextProposers, "current proposer and next proposer should be different"); + }); + it("should not get proposers for more than one epoch in the future", async function () { syncStub.isSynced.returns(true); server.sandbox.stub(chainStub.clock, "currentEpoch").get(() => 0); diff --git a/packages/lodestar/test/utils/mocks/chain/chain.ts b/packages/lodestar/test/utils/mocks/chain/chain.ts index fa713fbb8b35..229857dba231 100644 --- a/packages/lodestar/test/utils/mocks/chain/chain.ts +++ b/packages/lodestar/test/utils/mocks/chain/chain.ts @@ -2,7 +2,7 @@ import {AbortController} from "@chainsafe/abort-controller"; import sinon from "sinon"; import {toHexString, TreeBacked} from "@chainsafe/ssz"; -import {allForks, Epoch, Number64, Root, Slot, ssz, Uint16, Uint64, ValidatorIndex} from "@chainsafe/lodestar-types"; +import {allForks, Number64, Root, Slot, ssz, Uint16, Uint64} from "@chainsafe/lodestar-types"; import {IBeaconConfig} from "@chainsafe/lodestar-config"; import {CachedBeaconStateAllForks, createCachedBeaconState} from "@chainsafe/lodestar-beacon-state-transition"; import {phase0} from "@chainsafe/lodestar-beacon-state-transition"; @@ -81,8 +81,6 @@ export class MockBeaconChain implements IBeaconChain { readonly seenSyncCommitteeMessages = new SeenSyncCommitteeMessages(); readonly seenContributionAndProof = new SeenContributionAndProof(); - readonly nextEpochProposerDutyCache = new Map(); - private state: TreeBacked; private abortController: AbortController; @@ -178,10 +176,6 @@ export class MockBeaconChain implements IBeaconChain { persistInvalidSszObject(): string | null { return null; } - - getNextEpochProposerDuty(): Promise { - return Promise.resolve([]); - } } function mockForkChoice(): IForkChoice {