From bd7310f350f4c44c9cb47627bf33413a4e45b8aa Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Mon, 7 Oct 2024 15:11:27 +0100 Subject: [PATCH 01/11] feat: support fetching historical proposer duties --- .../src/api/impl/validator/index.ts | 34 +++++++++++++++---- .../impl/validator/duties/proposer.test.ts | 2 +- .../state-transition/src/cache/epochCache.ts | 4 +++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 2347d7086d46..b10d7a1af7bb 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -10,6 +10,7 @@ import { computeEpochAtSlot, getCurrentSlot, beaconBlockToBlinded, + createCachedBeaconState, } from "@lodestar/state-transition"; import { GENESIS_SLOT, @@ -61,7 +62,7 @@ import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/va import {CommitteeSubscription} from "../../../network/subnets/index.js"; import {ApiModules} from "../types.js"; import {RegenCaller} from "../../../chain/regen/index.js"; -import {getValidatorStatus} from "../beacon/state/utils.js"; +import {getStateResponseWithRegen, getValidatorStatus} from "../beacon/state/utils.js"; import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js"; import {SCHEDULER_LOOKAHEAD_FACTOR} from "../../../chain/prepareNextSlot.js"; import {ChainEvent, CheckpointHex, CommonBlockBody} from "../../../chain/index.js"; @@ -900,15 +901,16 @@ export function getValidatorApi( async getProposerDuties({epoch}) { notWhileSyncing(); - // Early check that epoch is within [current_epoch, current_epoch + 1], or allow for pre-genesis + // Early check that epoch is no more than current_epoch + 1, or allow for pre-genesis const currentEpoch = currentEpochWithDisparity(); const nextEpoch = currentEpoch + 1; - if (currentEpoch >= 0 && epoch !== currentEpoch && epoch !== nextEpoch) { - throw Error(`Requested epoch ${epoch} must equal current ${currentEpoch} or next epoch ${nextEpoch}`); + if (currentEpoch >= 0 && epoch > nextEpoch) { + throw new ApiError(400, `Requested epoch ${epoch} must not be more than one epoch in the future`); } const head = chain.forkChoice.getHead(); let state: CachedBeaconStateAllForks | undefined = undefined; + const startSlot = computeStartSlotAtEpoch(epoch); const slotMs = config.SECONDS_PER_SLOT * 1000; const prepareNextSlotLookAheadMs = slotMs / SCHEDULER_LOOKAHEAD_FACTOR; const toNextEpochMs = msToNextEpoch(); @@ -926,7 +928,22 @@ export function getValidatorApi( } if (!state) { - state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties); + if (epoch >= currentEpoch - 1) { + state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties); + } else { + const res = await getStateResponseWithRegen(chain, startSlot); + + const stateViewDU = + res.state instanceof Uint8Array + ? config.getForkTypes(startSlot).BeaconState.deserializeToViewDU(res.state) + : res.state; + + state = createCachedBeaconState(stateViewDU, { + config: chain.config, + pubkey2index: chain.pubkey2index, + index2pubkey: chain.index2pubkey, + }); + } } const stateEpoch = state.epochCtx.epoch; @@ -938,6 +955,12 @@ export function getValidatorApi( // Requesting duties for next epoch is allow since they can be predicted with high probabilities. // @see `epochCtx.getBeaconProposersNextEpoch` JSDocs for rationale. indexes = state.epochCtx.getBeaconProposersNextEpoch(); + } else if (epoch === stateEpoch - 1) { + const indexesPrevEpoch = state.epochCtx.getBeaconProposersPrevEpoch(); + if (indexesPrevEpoch === null) { + throw new ApiError(500, `Proposers duties for previous epoch ${epoch} not yet initialized`); + } + indexes = indexesPrevEpoch; } else { // Should never happen, epoch is checked to be in bounds above throw Error(`Proposer duties for epoch ${epoch} not supported, current epoch ${stateEpoch}`); @@ -949,7 +972,6 @@ export function getValidatorApi( // TODO: Add a flag to just send 0x00 as pubkeys since the Lodestar validator does not need them. const pubkeys = getPubkeysForIndices(state.validators, indexes); - const startSlot = computeStartSlotAtEpoch(epoch); const duties: routes.validator.ProposerDuty[] = []; for (let i = 0; i < SLOTS_PER_EPOCH; i++) { duties.push({slot: startSlot + i, validatorIndex: indexes[i], pubkey: pubkeys[i]}); diff --git a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts index b101382e01a0..9f60469dcc3c 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts @@ -84,7 +84,7 @@ describe("get proposers api impl", function () { it("should raise error for more than one epoch in the future", async () => { await expect(api.getProposerDuties({epoch: 2})).rejects.toThrow( - "Requested epoch 2 must equal current 0 or next epoch 1" + "Requested epoch 2 must not be more than one epoch in the future" ); }); diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 5e901e33d992..4eb16fa49927 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -877,6 +877,10 @@ export class EpochCache { return this.proposers; } + getBeaconProposersPrevEpoch(): ValidatorIndex[] | null { + return this.proposersPrevEpoch; + } + /** * We allow requesting proposal duties 1 epoch in the future as in normal network conditions it's possible to predict * the correct shuffling with high probability. While knowing the proposers in advance is not useful for consensus, From 35fa8e568a750887e93f856e604235d30ba2f34d Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 8 Oct 2024 13:09:22 +0100 Subject: [PATCH 02/11] Clone state before creating cached beacon state --- packages/beacon-node/src/api/impl/validator/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index b10d7a1af7bb..27596c96d010 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -936,7 +936,7 @@ export function getValidatorApi( const stateViewDU = res.state instanceof Uint8Array ? config.getForkTypes(startSlot).BeaconState.deserializeToViewDU(res.state) - : res.state; + : res.state.clone(); state = createCachedBeaconState(stateViewDU, { config: chain.config, From f80ffbbd2b087459846df5c96c9df293ac1aa01b Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 8 Oct 2024 13:12:56 +0100 Subject: [PATCH 03/11] Fix error message --- packages/beacon-node/src/api/impl/validator/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 27596c96d010..0e25fd847b0a 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -958,7 +958,7 @@ export function getValidatorApi( } else if (epoch === stateEpoch - 1) { const indexesPrevEpoch = state.epochCtx.getBeaconProposersPrevEpoch(); if (indexesPrevEpoch === null) { - throw new ApiError(500, `Proposers duties for previous epoch ${epoch} not yet initialized`); + throw new ApiError(500, `Proposer duties for previous epoch ${epoch} not yet initialized`); } indexes = indexesPrevEpoch; } else { From 757b166cc045991077c080153a41e2ebf8f18893 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 8 Oct 2024 13:14:47 +0100 Subject: [PATCH 04/11] Update test cases --- .../test/mocks/mockedBeaconChain.ts | 4 + .../impl/validator/duties/proposer.test.ts | 79 ++++++++++++++----- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index addeacf26a89..7a62e1a19630 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {vi, Mocked, Mock} from "vitest"; +import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {config as defaultConfig} from "@lodestar/config/default"; import {ChainForkConfig} from "@lodestar/config"; import {ForkChoice, ProtoBlock, EpochDifference} from "@lodestar/fork-choice"; @@ -129,6 +130,8 @@ vi.mock("../../src/chain/chain.js", async (importActual) => { // @ts-expect-error beaconProposerCache: new BeaconProposerCache(), shufflingCache: new ShufflingCache(), + pubkey2index: new PubkeyIndexMap(), + index2pubkey: [], produceCommonBlockBody: vi.fn(), getProposerHead: vi.fn(), produceBlock: vi.fn(), @@ -138,6 +141,7 @@ vi.mock("../../src/chain/chain.js", async (importActual) => { predictProposerHead: vi.fn(), getHeadStateAtCurrentEpoch: vi.fn(), getHeadState: vi.fn(), + getStateBySlot: vi.fn(), updateBuilderStatus: vi.fn(), processBlock: vi.fn(), regenStateForAttestationVerification: vi.fn(), diff --git a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts index 9f60469dcc3c..c650bb9e3cba 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts @@ -3,6 +3,7 @@ import {routes} from "@lodestar/api"; import {config} from "@lodestar/config/default"; import {MAX_EFFECTIVE_BALANCE, SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconStateAllForks} from "@lodestar/state-transition"; +import {Slot} from "@lodestar/types"; import {ApiTestModules, getApiTestModules} from "../../../../../utils/api.js"; import {FAR_FUTURE_EPOCH} from "../../../../../../src/constants/index.js"; import {SYNC_TOLERANCE_EPOCHS, getValidatorApi} from "../../../../../../src/api/impl/validator/index.js"; @@ -13,6 +14,9 @@ import {SyncState} from "../../../../../../src/sync/interface.js"; import {defaultApiOptions} from "../../../../../../src/api/options.js"; describe("get proposers api impl", function () { + const currentEpoch = 3; + const currentSlot = SLOTS_PER_EPOCH * currentEpoch; + let api: ReturnType; let modules: ApiTestModules; let state: BeaconStateAllForks; @@ -20,12 +24,24 @@ describe("get proposers api impl", function () { beforeEach(function () { vi.useFakeTimers({now: 0}); + vi.advanceTimersByTime(currentSlot * config.SECONDS_PER_SLOT * 1000); modules = getApiTestModules({clock: "real"}); api = getValidatorApi(defaultApiOptions, modules); + initializeState(currentSlot); + + modules.chain.getHeadStateAtCurrentEpoch.mockResolvedValue(cachedState); + modules.forkChoice.getHead.mockReturnValue(zeroProtoBlock); + modules.forkChoice.getFinalizedBlock.mockReturnValue(zeroProtoBlock); + modules.db.block.get.mockResolvedValue({message: {stateRoot: Buffer.alloc(32)}} as any); + + vi.spyOn(modules.sync, "state", "get").mockReturnValue(SyncState.Synced); + }); + + function initializeState(slot: Slot): void { state = generateState( { - slot: 0, + slot, validators: generateValidators(25, { effectiveBalance: MAX_EFFECTIVE_BALANCE, activationEpoch: 0, @@ -37,14 +53,10 @@ describe("get proposers api impl", function () { ); cachedState = createCachedBeaconStateTest(state, config); - modules.chain.getHeadStateAtCurrentEpoch.mockResolvedValue(cachedState); - modules.forkChoice.getHead.mockReturnValue(zeroProtoBlock); - modules.db.block.get.mockResolvedValue({message: {stateRoot: Buffer.alloc(32)}} as any); - - vi.spyOn(modules.sync, "state", "get").mockReturnValue(SyncState.Synced); vi.spyOn(cachedState.epochCtx, "getBeaconProposersNextEpoch"); vi.spyOn(cachedState.epochCtx, "getBeaconProposers"); - }); + vi.spyOn(cachedState.epochCtx, "getBeaconProposersPrevEpoch"); + } afterEach(() => { vi.useRealTimers(); @@ -54,7 +66,7 @@ describe("get proposers api impl", function () { vi.advanceTimersByTime((SYNC_TOLERANCE_EPOCHS * SLOTS_PER_EPOCH + 1) * config.SECONDS_PER_SLOT * 1000); vi.spyOn(modules.sync, "state", "get").mockReturnValue(SyncState.SyncingHead); - await expect(api.getProposerDuties({epoch: 1})).rejects.toThrow("Node is syncing - headSlot 0 currentSlot 9"); + await expect(api.getProposerDuties({epoch: 1})).rejects.toThrow("Node is syncing - headSlot 0 currentSlot 33"); }); it("should raise error if node stalled", async () => { @@ -65,34 +77,61 @@ describe("get proposers api impl", function () { }); it("should get proposers for current epoch", async () => { - const {data: result} = (await api.getProposerDuties({epoch: 0})) as {data: routes.validator.ProposerDutyList}; + const {data: result} = (await api.getProposerDuties({epoch: currentEpoch})) as { + data: routes.validator.ProposerDutyList; + }; expect(result.length).toBe(SLOTS_PER_EPOCH); expect(cachedState.epochCtx.getBeaconProposers).toHaveBeenCalledOnce(); expect(cachedState.epochCtx.getBeaconProposersNextEpoch).not.toHaveBeenCalled(); - expect(result.map((p) => p.slot)).toEqual(Array.from({length: SLOTS_PER_EPOCH}, (_, i) => i)); + expect(cachedState.epochCtx.getBeaconProposersPrevEpoch).not.toHaveBeenCalled(); + expect(result.map((p) => p.slot)).toEqual( + Array.from({length: SLOTS_PER_EPOCH}, (_, i) => currentEpoch * SLOTS_PER_EPOCH + i) + ); }); it("should get proposers for next epoch", async () => { - const {data: result} = (await api.getProposerDuties({epoch: 1})) as {data: routes.validator.ProposerDutyList}; + const nextEpoch = currentEpoch + 1; + const {data: result} = (await api.getProposerDuties({epoch: nextEpoch})) as { + data: routes.validator.ProposerDutyList; + }; expect(result.length).toBe(SLOTS_PER_EPOCH); expect(cachedState.epochCtx.getBeaconProposers).not.toHaveBeenCalled(); expect(cachedState.epochCtx.getBeaconProposersNextEpoch).toHaveBeenCalledOnce(); - expect(result.map((p) => p.slot)).toEqual(Array.from({length: SLOTS_PER_EPOCH}, (_, i) => SLOTS_PER_EPOCH + i)); + expect(cachedState.epochCtx.getBeaconProposersPrevEpoch).not.toHaveBeenCalled(); + expect(result.map((p) => p.slot)).toEqual( + Array.from({length: SLOTS_PER_EPOCH}, (_, i) => nextEpoch * SLOTS_PER_EPOCH + i) + ); + }); + + it("should get proposers for historical epoch", async () => { + const historicalEpoch = currentEpoch - 2; + initializeState(currentSlot - 2 * SLOTS_PER_EPOCH); + modules.chain.getStateBySlot.mockResolvedValue({state, executionOptimistic: false, finalized: true}); + + const {data: result} = (await api.getProposerDuties({epoch: historicalEpoch})) as { + data: routes.validator.ProposerDutyList; + }; + + expect(result.length).toBe(SLOTS_PER_EPOCH); + // Spy won't be called as `getProposerDuties` will create a new cached beacon state + expect(result.map((p) => p.slot)).toEqual( + Array.from({length: SLOTS_PER_EPOCH}, (_, i) => historicalEpoch * SLOTS_PER_EPOCH + i) + ); }); it("should raise error for more than one epoch in the future", async () => { - await expect(api.getProposerDuties({epoch: 2})).rejects.toThrow( - "Requested epoch 2 must not be more than one epoch in the future" + await expect(api.getProposerDuties({epoch: currentEpoch + 2})).rejects.toThrow( + "Requested epoch 5 must not be more than one epoch in the future" ); }); it("should have different proposer validator public keys for current and next epoch", async () => { - const {data: currentProposers} = (await api.getProposerDuties({epoch: 0})) as { + const {data: currentProposers} = (await api.getProposerDuties({epoch: currentEpoch})) as { data: routes.validator.ProposerDutyList; }; - const {data: nextProposers} = (await api.getProposerDuties({epoch: 1})) as { + const {data: nextProposers} = (await api.getProposerDuties({epoch: currentEpoch + 1})) as { data: routes.validator.ProposerDutyList; }; @@ -101,10 +140,10 @@ describe("get proposers api impl", function () { }); it("should have different proposer validator indexes for current and next epoch", async () => { - const {data: currentProposers} = (await api.getProposerDuties({epoch: 0})) as { + const {data: currentProposers} = (await api.getProposerDuties({epoch: currentEpoch})) as { data: routes.validator.ProposerDutyList; }; - const {data: nextProposers} = (await api.getProposerDuties({epoch: 1})) as { + const {data: nextProposers} = (await api.getProposerDuties({epoch: currentEpoch + 1})) as { data: routes.validator.ProposerDutyList; }; @@ -112,10 +151,10 @@ describe("get proposers api impl", function () { }); it("should have different proposer slots for current and next epoch", async () => { - const {data: currentProposers} = (await api.getProposerDuties({epoch: 0})) as { + const {data: currentProposers} = (await api.getProposerDuties({epoch: currentEpoch})) as { data: routes.validator.ProposerDutyList; }; - const {data: nextProposers} = (await api.getProposerDuties({epoch: 1})) as { + const {data: nextProposers} = (await api.getProposerDuties({epoch: currentEpoch + 1})) as { data: routes.validator.ProposerDutyList; }; From 3ac963140c74bdd6dfb5b85c8ef5eaee348968df Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 8 Oct 2024 13:47:22 +0100 Subject: [PATCH 05/11] Skip syncing pubkeys and sync committe cache --- .../beacon-node/src/api/impl/validator/index.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 0e25fd847b0a..30ad06b18b71 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1,3 +1,4 @@ +import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {routes} from "@lodestar/api"; import {ApplicationMethods} from "@lodestar/api/server"; import { @@ -938,11 +939,16 @@ export function getValidatorApi( ? config.getForkTypes(startSlot).BeaconState.deserializeToViewDU(res.state) : res.state.clone(); - state = createCachedBeaconState(stateViewDU, { - config: chain.config, - pubkey2index: chain.pubkey2index, - index2pubkey: chain.index2pubkey, - }); + state = createCachedBeaconState( + stateViewDU, + { + config: chain.config, + // Not required to compute proposers + pubkey2index: new PubkeyIndexMap(), + index2pubkey: [], + }, + {skipSyncPubkeys: true, skipSyncCommitteeCache: true} + ); } } From 695d952bbdcd6e5c3ba8e18ac59f56f1f652b1e2 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Tue, 8 Oct 2024 13:50:34 +0100 Subject: [PATCH 06/11] Update proposers tests epoch --- .../test/unit/api/impl/validator/duties/proposer.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts index c650bb9e3cba..49758c9bca58 100644 --- a/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts +++ b/packages/beacon-node/test/unit/api/impl/validator/duties/proposer.test.ts @@ -14,7 +14,7 @@ import {SyncState} from "../../../../../../src/sync/interface.js"; import {defaultApiOptions} from "../../../../../../src/api/options.js"; describe("get proposers api impl", function () { - const currentEpoch = 3; + const currentEpoch = 2; const currentSlot = SLOTS_PER_EPOCH * currentEpoch; let api: ReturnType; @@ -66,7 +66,7 @@ describe("get proposers api impl", function () { vi.advanceTimersByTime((SYNC_TOLERANCE_EPOCHS * SLOTS_PER_EPOCH + 1) * config.SECONDS_PER_SLOT * 1000); vi.spyOn(modules.sync, "state", "get").mockReturnValue(SyncState.SyncingHead); - await expect(api.getProposerDuties({epoch: 1})).rejects.toThrow("Node is syncing - headSlot 0 currentSlot 33"); + await expect(api.getProposerDuties({epoch: 1})).rejects.toThrow("Node is syncing - headSlot 0 currentSlot 25"); }); it("should raise error if node stalled", async () => { @@ -123,7 +123,7 @@ describe("get proposers api impl", function () { it("should raise error for more than one epoch in the future", async () => { await expect(api.getProposerDuties({epoch: currentEpoch + 2})).rejects.toThrow( - "Requested epoch 5 must not be more than one epoch in the future" + "Requested epoch 4 must not be more than one epoch in the future" ); }); From 14fe740150ac5197008bef5b7a2c7f888b4a0651 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 9 Oct 2024 10:30:34 +0100 Subject: [PATCH 07/11] Use switch/case instead of if/else --- .../src/api/impl/validator/index.ts | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index ac70e72abac9..ce5dab10b4ad 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -956,21 +956,29 @@ export function getValidatorApi( const stateEpoch = state.epochCtx.epoch; let indexes: ValidatorIndex[] = []; - if (epoch === stateEpoch) { - indexes = state.epochCtx.getBeaconProposers(); - } else if (epoch === stateEpoch + 1) { - // Requesting duties for next epoch is allow since they can be predicted with high probabilities. - // @see `epochCtx.getBeaconProposersNextEpoch` JSDocs for rationale. - indexes = state.epochCtx.getBeaconProposersNextEpoch(); - } else if (epoch === stateEpoch - 1) { - const indexesPrevEpoch = state.epochCtx.getBeaconProposersPrevEpoch(); - if (indexesPrevEpoch === null) { - throw new ApiError(500, `Proposer duties for previous epoch ${epoch} not yet initialized`); + switch (epoch) { + case stateEpoch: + indexes = state.epochCtx.getBeaconProposers(); + break; + + case stateEpoch + 1: + // Requesting duties for next epoch is allowed since they can be predicted with high probabilities. + // @see `epochCtx.getBeaconProposersNextEpoch` JSDocs for rationale. + indexes = state.epochCtx.getBeaconProposersNextEpoch(); + break; + + case stateEpoch - 1: { + const indexesPrevEpoch = state.epochCtx.getBeaconProposersPrevEpoch(); + if (indexesPrevEpoch === null) { + throw new ApiError(500, `Proposer duties for previous epoch ${epoch} not yet initialized`); + } + indexes = indexesPrevEpoch; + break; } - indexes = indexesPrevEpoch; - } else { - // Should never happen, epoch is checked to be in bounds above - throw Error(`Proposer duties for epoch ${epoch} not supported, current epoch ${stateEpoch}`); + + default: + // Should never happen, epoch is checked to be in bounds above + throw Error(`Proposer duties for epoch ${epoch} not supported, current epoch ${stateEpoch}`); } // NOTE: this is the fastest way of getting compressed pubkeys. From f85b86e91dcfef3a2e8c5683474c0f3e1e84fc66 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Wed, 9 Oct 2024 10:31:41 +0100 Subject: [PATCH 08/11] Add comment to clarify when head state can be used --- packages/beacon-node/src/api/impl/validator/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index ce5dab10b4ad..e0fd19226cb3 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -931,6 +931,8 @@ export function getValidatorApi( if (!state) { if (epoch >= currentEpoch - 1) { + // Cached beacon state stores proposers for previous, current and next epoch. The + // requested epoch is within that range, we can use the head state at current epoch state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.getDuties); } else { const res = await getStateResponseWithRegen(chain, startSlot); From 13002fd75d86b4b74c41173c8dc820cfc845b125 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 10 Oct 2024 14:35:24 +0100 Subject: [PATCH 09/11] Use loadState instead of creating a separate ViewDU --- packages/beacon-node/src/api/impl/validator/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index e0fd19226cb3..8855048d86a8 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -12,6 +12,7 @@ import { getCurrentSlot, beaconBlockToBlinded, createCachedBeaconState, + loadState, } from "@lodestar/state-transition"; import { GENESIS_SLOT, @@ -939,7 +940,7 @@ export function getValidatorApi( const stateViewDU = res.state instanceof Uint8Array - ? config.getForkTypes(startSlot).BeaconState.deserializeToViewDU(res.state) + ? loadState(config, chain.getHeadState(), res.state).state : res.state.clone(); state = createCachedBeaconState( From 6f237034315efb4aa25f97bbde40d33986ace396 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Thu, 10 Oct 2024 14:44:32 +0100 Subject: [PATCH 10/11] Clarify not yet initialized error for prev proposer duties --- packages/beacon-node/src/api/impl/validator/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 8855048d86a8..43bb60187463 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -973,7 +973,9 @@ export function getValidatorApi( case stateEpoch - 1: { const indexesPrevEpoch = state.epochCtx.getBeaconProposersPrevEpoch(); if (indexesPrevEpoch === null) { - throw new ApiError(500, `Proposer duties for previous epoch ${epoch} not yet initialized`); + // Should not happen as previous proposer duties should be initialized for head state + // and if we load state from `Uint8Array` it will always be the state of requested epoch + throw Error(`Proposer duties for previous epoch ${epoch} not yet initialized`); } indexes = indexesPrevEpoch; break; From 439d26a9081b46d4f17ab2fa96260bd18434ae20 Mon Sep 17 00:00:00 2001 From: Nico Flaig Date: Fri, 11 Oct 2024 13:48:26 +0100 Subject: [PATCH 11/11] Assert loaded state epoch matches requested --- packages/beacon-node/src/api/impl/validator/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 1c9c74a66ba9..9c5e6c2987f1 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -941,6 +941,10 @@ export function getValidatorApi( }, {skipSyncPubkeys: true, skipSyncCommitteeCache: true} ); + + if (state.epochCtx.epoch !== epoch) { + throw Error(`Loaded state epoch ${state.epochCtx.epoch} does not match requested epoch ${epoch}`); + } } }