diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 371ab178c14e..0256d23cedc0 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -1808,7 +1808,7 @@ export function getValidatorApi( const filteredRegistrations = registrations.filter((registration) => { const {pubkey} = registration.message; const validatorIndex = chain.pubkeyCache.getIndex(pubkey); - if (validatorIndex === null) return false; + if (validatorIndex === null || validatorIndex >= headState.validatorCount) return false; const validator = headState.getValidator(validatorIndex); const status = getValidatorStatus(validator, currentEpoch); diff --git a/packages/state-transition/src/block/processWithdrawalRequest.ts b/packages/state-transition/src/block/processWithdrawalRequest.ts index a91c745d91a0..245f5ce9658d 100644 --- a/packages/state-transition/src/block/processWithdrawalRequest.ts +++ b/packages/state-transition/src/block/processWithdrawalRequest.ts @@ -8,7 +8,11 @@ import { import {electra, phase0, ssz} from "@lodestar/types"; import {toHex} from "@lodestar/utils"; import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js"; -import {hasCompoundingWithdrawalCredential, hasExecutionWithdrawalCredential} from "../util/electra.js"; +import { + hasCompoundingWithdrawalCredential, + hasExecutionWithdrawalCredential, + isValidatorKnown, +} from "../util/electra.js"; import {computeExitEpochAndUpdateChurn} from "../util/epoch.js"; import {getPendingBalanceToWithdraw, isActiveValidator} from "../util/validator.js"; import {initiateValidatorExit} from "./initiateValidatorExit.js"; @@ -32,7 +36,7 @@ export function processWithdrawalRequest( // bail out if validator is not in beacon state // note that we don't need to check for 6110 unfinalized vals as they won't be eligible for withdraw/exit anyway const validatorIndex = pubkeyCache.getIndex(withdrawalRequest.validatorPubkey); - if (validatorIndex === null) { + if (!isValidatorKnown(state, validatorIndex)) { return; } diff --git a/packages/state-transition/test/unit/block/processWithdrawalRequest.test.ts b/packages/state-transition/test/unit/block/processWithdrawalRequest.test.ts new file mode 100644 index 000000000000..948707d30c92 --- /dev/null +++ b/packages/state-transition/test/unit/block/processWithdrawalRequest.test.ts @@ -0,0 +1,26 @@ +import {describe, expect, it} from "vitest"; +import {fromHexString} from "@chainsafe/ssz"; +import {ForkSeq} from "@lodestar/params"; +import {ssz} from "@lodestar/types"; +import {processWithdrawalRequest} from "../../../src/block/processWithdrawalRequest.js"; +import {generateCachedElectraState} from "../../utils/state.js"; + +const futureValidatorPubkey = fromHexString( + "0xa41726266b1d83ef609d759ba7796d54cfe549154e01e4730a3378309bc81a7638140d7e184b33593c072595f23f032d" +); + +describe("processWithdrawalRequest", () => { + it("ignores a validator present only in the shared pubkey cache", () => { + const state = generateCachedElectraState(); + const futureState = state.clone(); + const futureValidatorIndex = state.validators.length; + futureState.epochCtx.addPubkey(futureValidatorIndex, futureValidatorPubkey); + + const request = ssz.electra.WithdrawalRequest.defaultValue(); + request.validatorPubkey = futureValidatorPubkey; + + expect(state.epochCtx.getValidatorIndex(futureValidatorPubkey)).toBe(futureValidatorIndex); + expect(() => processWithdrawalRequest(ForkSeq.electra, state, request)).not.toThrow(); + expect(state.pendingPartialWithdrawals.length).toBe(0); + }); +});