diff --git a/packages/beacon-node/src/api/impl/beacon/pool/index.ts b/packages/beacon-node/src/api/impl/beacon/pool/index.ts index 2e0fea1aa66e..049eea32f8c9 100644 --- a/packages/beacon-node/src/api/impl/beacon/pool/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/pool/index.ts @@ -212,7 +212,21 @@ export function getBeaconPoolApi({ }, async submitPoolVoluntaryExit({signedVoluntaryExit}) { - await validateApiVoluntaryExit(chain, signedVoluntaryExit); + const result = await validateApiVoluntaryExit(chain, signedVoluntaryExit); + + if (result.status === "deferred") { + const currentEpoch = chain.clock.currentEpoch; + const inserted = chain.deferredVoluntaryExitPool.insert(signedVoluntaryExit, result.validity, currentEpoch); + if (!inserted) { + throw new ApiError(400, "Deferred voluntary exit pool is full or already contains this validator"); + } + logger.info("Voluntary exit deferred until transient conditions are met", { + validatorIndex: signedVoluntaryExit.message.validatorIndex, + reason: result.validity, + }); + return; + } + chain.opPool.insertVoluntaryExit(signedVoluntaryExit); chain.emitter.emit(routes.events.EventType.voluntaryExit, signedVoluntaryExit); await network.publishVoluntaryExit(signedVoluntaryExit); diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 213d7cc5c341..aa8548a30523 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -84,6 +84,7 @@ import {LightClientServer} from "./lightClient/index.js"; import { AggregatedAttestationPool, AttestationPool, + DeferredVoluntaryExitPool, ExecutionPayloadBidPool, OpPool, PayloadAttestationPool, @@ -180,6 +181,7 @@ export class BeaconChain implements IBeaconChain { readonly executionPayloadBidPool: ExecutionPayloadBidPool; readonly payloadAttestationPool: PayloadAttestationPool; readonly opPool: OpPool; + readonly deferredVoluntaryExitPool: DeferredVoluntaryExitPool; // Gossip seen cache readonly seenAttesters = new SeenAttesters(); @@ -306,6 +308,7 @@ export class BeaconChain implements IBeaconChain { this.executionPayloadBidPool = new ExecutionPayloadBidPool(); this.payloadAttestationPool = new PayloadAttestationPool(config, clock, metrics); this.opPool = new OpPool(config); + this.deferredVoluntaryExitPool = new DeferredVoluntaryExitPool(logger); this.seenAggregatedAttestations = new SeenAggregatedAttestations(metrics); this.seenContributionAndProof = new SeenContributionAndProof(metrics); @@ -1406,6 +1409,7 @@ export class BeaconChain implements IBeaconChain { // aggregatedAttestationPool tracks metrics on its own metrics.opPool.attestationPool.size.set(this.attestationPool.getAttestationCount()); metrics.opPool.attesterSlashingPoolSize.set(this.opPool.attesterSlashingsSize); + metrics.opPool.deferredVoluntaryExitPool.size.set(this.deferredVoluntaryExitPool.size()); metrics.opPool.proposerSlashingPoolSize.set(this.opPool.proposerSlashingsSize); metrics.opPool.voluntaryExitPoolSize.set(this.opPool.voluntaryExitsSize); metrics.opPool.syncCommitteeMessagePoolSize.set(this.syncCommitteeMessagePool.size); diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index b13304a8a08f..b5def0fd9c49 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -44,6 +44,7 @@ import {LightClientServer} from "./lightClient/index.js"; import {AggregatedAttestationPool} from "./opPools/aggregatedAttestationPool.js"; import { AttestationPool, + DeferredVoluntaryExitPool, ExecutionPayloadBidPool, OpPool, PayloadAttestationPool, @@ -125,6 +126,7 @@ export interface IBeaconChain { readonly executionPayloadBidPool: ExecutionPayloadBidPool; readonly payloadAttestationPool: PayloadAttestationPool; readonly opPool: OpPool; + readonly deferredVoluntaryExitPool: DeferredVoluntaryExitPool; // Gossip seen cache readonly seenAttesters: SeenAttesters; diff --git a/packages/beacon-node/src/chain/opPools/deferredVoluntaryExitPool.ts b/packages/beacon-node/src/chain/opPools/deferredVoluntaryExitPool.ts new file mode 100644 index 000000000000..ce398aec2d71 --- /dev/null +++ b/packages/beacon-node/src/chain/opPools/deferredVoluntaryExitPool.ts @@ -0,0 +1,58 @@ +import {Logger} from "@lodestar/logger"; +import {IBeaconStateView, VoluntaryExitValidity, isTransientExitValidity} from "@lodestar/state-transition"; +import {Epoch, ValidatorIndex} from "@lodestar/types"; +import {SignedVoluntaryExit} from "@lodestar/types/phase0"; + +type DeferredEntry = { + exit: SignedVoluntaryExit; + validity: VoluntaryExitValidity; + insertedAtEpoch: Epoch; +}; + +export class DeferredVoluntaryExitPool { + private pool = new Map(); + + constructor( + private readonly logger: Logger, + private readonly maxSize = 1024, + private readonly maxDeferEpochs = 256 + ) {} + + insert(exit: SignedVoluntaryExit, validity: VoluntaryExitValidity, currentEpoch: Epoch): boolean { + if (!isTransientExitValidity(validity)) return false; + if (this.pool.size === this.maxSize) return false; + if (this.pool.has(exit.message.validatorIndex)) return false; + + this.pool.set(exit.message.validatorIndex, {exit, validity, insertedAtEpoch: currentEpoch}); + + return true; + } + + retrieveProcessableExits(state: IBeaconStateView): SignedVoluntaryExit[] { + const epoch = state.epoch; + const validExits: SignedVoluntaryExit[] = []; + for (const [validatorIndex, entry] of this.pool) { + try { + if (epoch - entry.insertedAtEpoch > this.maxDeferEpochs) { + this.pool.delete(validatorIndex); + continue; + } + const validity = state.getVoluntaryExitValidity(entry.exit, false); + if (validity === VoluntaryExitValidity.valid) { + validExits.push(entry.exit); + this.pool.delete(validatorIndex); + } else if (!isTransientExitValidity(validity)) { + this.pool.delete(validatorIndex); + } + // Else if still transient - keep + } catch (e) { + this.logger.debug("Processing deferred voluntary exit failed", {validatorIndex}, e as Error); + } + } + return validExits; + } + + size(): number { + return this.pool.size; + } +} diff --git a/packages/beacon-node/src/chain/opPools/index.ts b/packages/beacon-node/src/chain/opPools/index.ts index 262fb9419856..32e86104c882 100644 --- a/packages/beacon-node/src/chain/opPools/index.ts +++ b/packages/beacon-node/src/chain/opPools/index.ts @@ -1,5 +1,6 @@ export {AggregatedAttestationPool} from "./aggregatedAttestationPool.js"; export {AttestationPool} from "./attestationPool.js"; +export {DeferredVoluntaryExitPool} from "./deferredVoluntaryExitPool.js"; export {ExecutionPayloadBidPool} from "./executionPayloadBidPool.js"; export {OpPool} from "./opPool.js"; export {PayloadAttestationPool} from "./payloadAttestationPool.js"; diff --git a/packages/beacon-node/src/chain/regen/interface.ts b/packages/beacon-node/src/chain/regen/interface.ts index b70844f0fb59..e55acf183200 100644 --- a/packages/beacon-node/src/chain/regen/interface.ts +++ b/packages/beacon-node/src/chain/regen/interface.ts @@ -20,6 +20,8 @@ export enum RegenCaller { validateGossipAggregateAndProof = "validateGossipAggregateAndProof", validateGossipAttestation = "validateGossipAttestation", validateGossipVoluntaryExit = "validateGossipVoluntaryExit", + validateApiVoluntaryExit = "validateApiVoluntaryExit", + publishDeferredVoluntaryExits = "publishDeferredVoluntaryExits", validateGossipExecutionPayloadBid = "validateGossipExecutionPayloadBid", validateGossipProposerPreferences = "validateGossipProposerPreferences", onForkChoiceFinalized = "onForkChoiceFinalized", diff --git a/packages/beacon-node/src/chain/validation/voluntaryExit.ts b/packages/beacon-node/src/chain/validation/voluntaryExit.ts index 5f7a5538b962..2c5ea9195b99 100644 --- a/packages/beacon-node/src/chain/validation/voluntaryExit.ts +++ b/packages/beacon-node/src/chain/validation/voluntaryExit.ts @@ -1,4 +1,4 @@ -import {VoluntaryExitValidity, getVoluntaryExitSignatureSet} from "@lodestar/state-transition"; +import {VoluntaryExitValidity, getVoluntaryExitSignatureSet, isTransientExitValidity} from "@lodestar/state-transition"; import {phase0} from "@lodestar/types"; import { GossipAction, @@ -9,12 +9,50 @@ import { import {IBeaconChain} from "../index.js"; import {RegenCaller} from "../regen/index.js"; +export type ApiVoluntaryExitResult = {status: "published"} | {status: "deferred"; validity: VoluntaryExitValidity}; + +// Comments for each call are present inside `validateVoluntaryExit`. export async function validateApiVoluntaryExit( chain: IBeaconChain, voluntaryExit: phase0.SignedVoluntaryExit -): Promise { +): Promise { const prioritizeBls = true; - return validateVoluntaryExit(chain, voluntaryExit, prioritizeBls); + + if (chain.opPool.hasSeenVoluntaryExit(voluntaryExit.message.validatorIndex)) { + throw new VoluntaryExitError(GossipAction.IGNORE, { + code: VoluntaryExitErrorCode.ALREADY_EXISTS, + }); + } + + const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.validateApiVoluntaryExit); + + if (voluntaryExit.message.validatorIndex >= state.validatorCount) { + throw new VoluntaryExitError(GossipAction.REJECT, { + code: VoluntaryExitErrorCode.INACTIVE, + }); + } + + const validity = state.getVoluntaryExitValidity(voluntaryExit, false); + + if (validity !== VoluntaryExitValidity.valid && !isTransientExitValidity(validity)) { + throw new VoluntaryExitError(GossipAction.REJECT, { + code: voluntaryExitValidityToErrorCode(validity), + }); + } + + const signatureSet = getVoluntaryExitSignatureSet(chain.config, state, voluntaryExit); + if (!(await chain.bls.verifySignatureSets([signatureSet], {batchable: true, priority: prioritizeBls}))) { + throw new VoluntaryExitError(GossipAction.REJECT, { + code: VoluntaryExitErrorCode.INVALID_SIGNATURE, + }); + } + + if (validity !== VoluntaryExitValidity.valid) { + // Transient failure — signature is good, defer + return {status: "deferred", validity}; + } + + return {status: "published"}; } export async function validateGossipVoluntaryExit( diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 44df4161692d..444e17bf492c 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -1157,6 +1157,12 @@ export function createLodestarMetrics( name: "lodestar_oppool_attester_slashing_pool_size", help: "Current size of the AttesterSlashingPool", }), + deferredVoluntaryExitPool: { + size: register.gauge({ + name: "lodestar_oppool_deferred_voluntary_exit_pool_size", + help: "Current size of the DeferredVoluntaryExitPool", + }), + }, proposerSlashingPoolSize: register.gauge({ name: "lodestar_oppool_proposer_slashing_pool_size", help: "Current size of the ProposerSlashingPool", diff --git a/packages/beacon-node/src/node/nodejs.ts b/packages/beacon-node/src/node/nodejs.ts index 29189ce3bcce..5f39a405e173 100644 --- a/packages/beacon-node/src/node/nodejs.ts +++ b/packages/beacon-node/src/node/nodejs.ts @@ -2,6 +2,7 @@ import {setMaxListeners} from "node:events"; import {PrivateKey} from "@libp2p/interface"; import {Registry} from "prom-client"; import {hasher} from "@chainsafe/persistent-merkle-tree"; +import {routes} from "@lodestar/api"; import {BeaconApiMethods} from "@lodestar/api/beacon/server"; import {BeaconConfig} from "@lodestar/config"; import type {LoggerNode} from "@lodestar/logger/node"; @@ -12,6 +13,7 @@ import {sleep, toRootHex} from "@lodestar/utils"; import {ProcessShutdownCallback} from "@lodestar/validator"; import {BeaconRestApiServer, getApi} from "../api/index.js"; import {BeaconChain, IBeaconChain, initBeaconMetrics} from "../chain/index.js"; +import {RegenCaller} from "../chain/regen/index.js"; import {ValidatorMonitor, createValidatorMonitor} from "../chain/validatorMonitor.js"; import {IBeaconDb} from "../db/index.js"; import {initializeExecutionBuilder, initializeExecutionEngine} from "../execution/index.js"; @@ -20,7 +22,7 @@ import {MonitoringService} from "../monitoring/index.js"; import {Network, getReqRespHandlers} from "../network/index.js"; import {BackfillSync} from "../sync/backfill/index.js"; import {BeaconSync, IBeaconSync} from "../sync/index.js"; -import {Clock} from "../util/clock.js"; +import {Clock, ClockEvent} from "../util/clock.js"; import {runNodeNotifier} from "./notifier.js"; import {IBeaconNodeOptions} from "./options.js"; @@ -334,6 +336,31 @@ export class BeaconNode { void runNodeNotifier({network, chain, sync, config, logger, signal}); + chain.clock.addListener(ClockEvent.epoch, async () => { + try { + const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.publishDeferredVoluntaryExits); + const exits = chain.deferredVoluntaryExitPool.retrieveProcessableExits(state); + for (const exit of exits) { + try { + chain.opPool.insertVoluntaryExit(exit); + chain.emitter.emit(routes.events.EventType.voluntaryExit, exit); + await network.publishVoluntaryExit(exit); + logger.info("Voluntary exit successfully published for validator", { + validatorIndex: exit.message.validatorIndex, + }); + } catch (e) { + logger.warn( + "Failed to publish deferred voluntary exit", + {validatorIndex: exit.message.validatorIndex}, + e as Error + ); + } + } + } catch (e) { + logger.warn("Failed to drain deferred voluntary exit pool", {}, e as Error); + } + }); + return new BeaconNode({ opts, config, diff --git a/packages/beacon-node/test/unit/chain/opPools/deferredVoluntaryExitPool.test.ts b/packages/beacon-node/test/unit/chain/opPools/deferredVoluntaryExitPool.test.ts new file mode 100644 index 000000000000..9aa31f31126f --- /dev/null +++ b/packages/beacon-node/test/unit/chain/opPools/deferredVoluntaryExitPool.test.ts @@ -0,0 +1,185 @@ +import {beforeEach, describe, expect, it} from "vitest"; +import {IBeaconStateView, VoluntaryExitValidity} from "@lodestar/state-transition"; +import {phase0, ssz} from "@lodestar/types"; +import {DeferredVoluntaryExitPool} from "../../../../src/chain/opPools/deferredVoluntaryExitPool.js"; +import {getMockedLogger} from "../../../mocks/loggerMock.js"; + +function makeStateStub( + epoch: number, + validityFn: (exit: phase0.SignedVoluntaryExit) => VoluntaryExitValidity +): IBeaconStateView { + return { + epoch, + getVoluntaryExitValidity: validityFn, + } as unknown as IBeaconStateView; +} + +describe("DeferredVoluntaryExitPool", () => { + const logger = getMockedLogger(); + const maxSize = 256; + const maxDeferEpochs = 256; + const epoch = 1; + + let pool: DeferredVoluntaryExitPool; + + beforeEach(() => { + pool = new DeferredVoluntaryExitPool(logger, maxSize, maxDeferEpochs); + }); + + describe("insert", () => { + it("correct empty state", () => { + expect(pool.size()).toBe(0); + }); + + it("transiently invalid exit insert succeeds", () => { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + expect(pool.size()).toBe(1); + }); + + it("permanently invalid exit insert fails", () => { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const result = pool.insert(exit, VoluntaryExitValidity.invalidSignature, epoch); + expect(result).toBe(false); + expect(pool.size()).toBe(0); + }); + + it("valid exit insert fails", () => { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const result = pool.insert(exit, VoluntaryExitValidity.valid, epoch); + expect(result).toBe(false); + expect(pool.size()).toBe(0); + }); + + it("transiently invalid exit - rejects a duplicate", () => { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + let result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + expect(pool.size()).toBe(1); + result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(false); + expect(pool.size()).toBe(1); + }); + + it("accepts inserts up to maxSize, rejects beyond", () => { + for (let i = 0; i < maxSize; i++) { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + exit.message.validatorIndex = i; + const result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + expect(pool.size()).toBe(i + 1); + } + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + exit.message.validatorIndex = maxSize; + const result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(false); + expect(pool.size()).toBe(maxSize); + }); + }); + + describe("retrieveProcessableExits", () => { + it("correct empty state", () => { + const mockState = makeStateStub(epoch, () => VoluntaryExitValidity.valid); + expect(pool.retrieveProcessableExits(mockState)).toEqual([]); + }); + + it("valid entry is retrieved and removed", () => { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + expect(pool.size()).toBe(1); + const mockState = makeStateStub(epoch, () => VoluntaryExitValidity.valid); + expect(pool.retrieveProcessableExits(mockState)).toEqual([exit]); + expect(pool.size()).toBe(0); + }); + + it("transiently invalid entry is unprocessed and kept", () => { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + expect(pool.size()).toBe(1); + const mockState = makeStateStub(epoch, () => VoluntaryExitValidity.shortTimeActive); + expect(pool.retrieveProcessableExits(mockState)).toEqual([]); + expect(pool.size()).toBe(1); + }); + + it("permanently invalid entry is removed", () => { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + expect(pool.size()).toBe(1); + const mockState = makeStateStub(epoch, () => VoluntaryExitValidity.invalidSignature); + expect(pool.retrieveProcessableExits(mockState)).toEqual([]); + expect(pool.size()).toBe(0); + }); + + it("an entry past deferred ceiling is removed", () => { + const exit = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const result = pool.insert(exit, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + expect(pool.size()).toBe(1); + const mockState = makeStateStub(maxDeferEpochs + epoch + 1, () => VoluntaryExitValidity.valid); + expect(pool.retrieveProcessableExits(mockState)).toEqual([]); + expect(pool.size()).toBe(0); + }); + + it("does not stop processing when an entry throws", () => { + const exit1 = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const exit2 = ssz.phase0.SignedVoluntaryExit.defaultValue(); + exit2.message.validatorIndex = 1; + + pool.insert(exit1, VoluntaryExitValidity.shortTimeActive, epoch); + pool.insert(exit2, VoluntaryExitValidity.shortTimeActive, epoch); + + const validityFn = (exit: phase0.SignedVoluntaryExit): VoluntaryExitValidity => { + if (exit.message.validatorIndex === 0) { + throw new Error("boom"); + } + return VoluntaryExitValidity.valid; + }; + + const mockState = makeStateStub(epoch, validityFn); + expect(pool.retrieveProcessableExits(mockState)).toEqual([exit2]); + // Exit1 stays in pool because the throw was caught before delete + expect(pool.size()).toBe(1); + }); + + it("process 3 different cases of exits at once", () => { + const exit1 = ssz.phase0.SignedVoluntaryExit.defaultValue(); + const exit2 = ssz.phase0.SignedVoluntaryExit.defaultValue(); + exit2.message.validatorIndex = 1; + const exit3 = ssz.phase0.SignedVoluntaryExit.defaultValue(); + exit3.message.validatorIndex = 2; + + let result = pool.insert(exit1, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + result = pool.insert(exit2, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + result = pool.insert(exit3, VoluntaryExitValidity.shortTimeActive, epoch); + expect(result).toBe(true); + + expect(pool.size()).toBe(3); + + const validityFn = (exit: phase0.SignedVoluntaryExit) => { + // This ordering helps us make sure that invalid exits do not prevent + // the valid test from being retrieved + switch (exit.message.validatorIndex) { + case 0: + return VoluntaryExitValidity.invalidSignature; + case 1: + return VoluntaryExitValidity.shortTimeActive; + default: + return VoluntaryExitValidity.valid; + } + }; + + const mockState = makeStateStub(epoch, validityFn); + // Exit 1 is returned and removed + // Exit 2 is kept + // Exit 3 is removed + expect(pool.retrieveProcessableExits(mockState)).toEqual([exit3]); + expect(pool.size()).toBe(1); + }); + }); +}); diff --git a/packages/state-transition/src/block/processVoluntaryExit.ts b/packages/state-transition/src/block/processVoluntaryExit.ts index 27f823c1dafb..063bdb85352d 100644 --- a/packages/state-transition/src/block/processVoluntaryExit.ts +++ b/packages/state-transition/src/block/processVoluntaryExit.ts @@ -23,6 +23,24 @@ export enum VoluntaryExitValidity { invalidSignature = "invalid_signature", } +// Variants that can become valid in a future epoch without user action. +// - earlyEpoch: exit.message.epoch is in the future; valid once current epoch catches up +// - shortTimeActive: validator active < SHARD_COMMITTEE_PERIOD; valid once enough time passes +// - pendingWithdrawals: Electra; valid once pending partial withdrawals drain +const TRANSIENT_EXIT_VALIDITY = new Set([ + VoluntaryExitValidity.earlyEpoch, + VoluntaryExitValidity.shortTimeActive, + VoluntaryExitValidity.pendingWithdrawals, +]); +// Note: VoluntaryExitValidity.inactive is intentionally excluded. It conflates +// "validator does not exist" (permanent) with "validator not yet activated" +// (transient), and cleanly classifying it requires splitting the enum variant +// upstream. Left for a future follow-up. + +export function isTransientExitValidity(v: VoluntaryExitValidity): boolean { + return TRANSIENT_EXIT_VALIDITY.has(v); +} + /** * Process a VoluntaryExit operation. Initiates the exit of a validator or builder. * diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 5fd6b6be2316..e834f743698b 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -10,7 +10,12 @@ export {isValidBlsToExecutionChange} from "./block/processBlsToExecutionChange.j export {becomesNewEth1Data} from "./block/processEth1Data.js"; export {assertValidProposerSlashing} from "./block/processProposerSlashing.js"; // BeaconChain validation -export {VoluntaryExitValidity, getVoluntaryExitValidity, isValidVoluntaryExit} from "./block/processVoluntaryExit.js"; +export { + VoluntaryExitValidity, + getVoluntaryExitValidity, + isTransientExitValidity, + isValidVoluntaryExit, +} from "./block/processVoluntaryExit.js"; // Withdrawals for new blocks export {getExpectedWithdrawals} from "./block/processWithdrawals.js"; export {ProposerRewardType} from "./block/types.js";