From b78cb92802cfc12c3a75f33a053055e3db5a7c45 Mon Sep 17 00:00:00 2001 From: NC <17676176+ensi321@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:20:52 +0700 Subject: [PATCH] feat: remove unfinalized pubkey cache (#7230) * Remove unfinalized pubkey cache * lint * Fix unit test --- packages/beacon-node/src/chain/chain.ts | 22 --- .../beacon-node/src/chain/regen/queued.ts | 50 +----- .../chain/stateCache/blockStateCacheImpl.ts | 2 +- .../beacon-node/src/metrics/metrics/beacon.ts | 7 - .../src/metrics/metrics/lodestar.ts | 11 -- .../test/memory/unfinalizedPubkey2Index.ts | 54 ------- .../updateUnfinalizedPubkeys.test.ts | 114 -------------- .../test/sim/electra-interop.test.ts | 31 +--- packages/state-transition/package.json | 3 +- .../state-transition/src/cache/epochCache.ts | 146 +----------------- .../state-transition/src/cache/pubkeyCache.ts | 36 ----- packages/state-transition/src/index.ts | 7 +- packages/state-transition/src/metrics.ts | 5 - .../test/unit/cachedBeaconState.test.ts | 36 +---- yarn.lock | 5 - 15 files changed, 15 insertions(+), 514 deletions(-) delete mode 100644 packages/beacon-node/test/memory/unfinalizedPubkey2Index.ts delete mode 100644 packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 8b9fa0336283..4751770b3bfc 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -1047,9 +1047,6 @@ export class BeaconChain implements IBeaconChain { metrics.forkChoice.balancesLength.set(forkChoiceMetrics.balancesLength); metrics.forkChoice.nodes.set(forkChoiceMetrics.nodes); metrics.forkChoice.indices.set(forkChoiceMetrics.indices); - - const headState = this.getHeadState(); - metrics.headState.unfinalizedPubkeyCacheSize.set(headState.epochCtx.unfinalizedPubkey2index.size); } private onClockSlot(slot: Slot): void { @@ -1139,27 +1136,8 @@ export class BeaconChain implements IBeaconChain { this.opPool.pruneAll(headBlock, headState); } - const cpEpoch = cp.epoch; - if (headState === null) { this.logger.verbose("Head state is null"); - } else if (cpEpoch >= this.config.ELECTRA_FORK_EPOCH) { - // Get the validator.length from the state at cpEpoch - // We are confident the last element in the list is from headEpoch - // Thus we query from the end of the list. (cpEpoch - headEpoch - 1) is negative number - const pivotValidatorIndex = headState.epochCtx.getValidatorCountAtEpoch(cpEpoch); - - if (pivotValidatorIndex !== undefined) { - // Note EIP-6914 will break this logic - const newFinalizedValidators = headState.epochCtx.unfinalizedPubkey2index.filter( - (index, _pubkey) => index < pivotValidatorIndex - ); - - // Populate finalized pubkey cache and remove unfinalized pubkey cache - if (!newFinalizedValidators.isEmpty()) { - this.regen.updateUnfinalizedPubkeys(newFinalizedValidators); - } - } } // TODO-Electra: Deprecating eth1Data poll requires a check on a finalized checkpoint state. diff --git a/packages/beacon-node/src/chain/regen/queued.ts b/packages/beacon-node/src/chain/regen/queued.ts index 7b812c04dc8d..b5084d593356 100644 --- a/packages/beacon-node/src/chain/regen/queued.ts +++ b/packages/beacon-node/src/chain/regen/queued.ts @@ -1,6 +1,6 @@ import {routes} from "@lodestar/api"; import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; -import {CachedBeaconStateAllForks, UnfinalizedPubkeyIndexMap, computeEpochAtSlot} from "@lodestar/state-transition"; +import {CachedBeaconStateAllForks, computeEpochAtSlot} from "@lodestar/state-transition"; import {BeaconBlock, Epoch, RootHex, Slot, phase0} from "@lodestar/types"; import {Logger, toRootHex} from "@lodestar/utils"; import {Metrics} from "../../metrics/index.js"; @@ -206,54 +206,6 @@ export class QueuedStateRegenerator implements IStateRegenerator { return this.checkpointStateCache.updatePreComputedCheckpoint(rootHex, epoch); } - /** - * Remove `validators` from all unfinalized cache's epochCtx.UnfinalizedPubkey2Index, - * and add them to epochCtx.pubkey2index and epochCtx.index2pubkey - */ - updateUnfinalizedPubkeys(validators: UnfinalizedPubkeyIndexMap): void { - let numStatesUpdated = 0; - const states = this.blockStateCache.getStates(); - const cpStates = this.checkpointStateCache.getStates(); - - // Add finalized pubkeys to all states. - const addTimer = this.metrics?.regenFnAddPubkeyTime.startTimer(); - - // We only need to add pubkeys to any one of the states since the finalized caches is shared globally across all states - const firstState = (states.next().value ?? cpStates.next().value) as CachedBeaconStateAllForks | undefined; - - if (firstState !== undefined) { - firstState.epochCtx.addFinalizedPubkeys(validators, this.metrics?.epochCache ?? undefined); - } else { - this.logger.warn("Attempt to delete finalized pubkey from unfinalized pubkey cache. But no state is available"); - } - - addTimer?.(); - - // Delete finalized pubkeys from unfinalized pubkey cache for all states - const deleteTimer = this.metrics?.regenFnDeletePubkeyTime.startTimer(); - const pubkeysToDelete = Array.from(validators.keys()); - - for (const s of states) { - s.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); - numStatesUpdated++; - } - - for (const s of cpStates) { - s.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); - numStatesUpdated++; - } - - // Since first state is consumed from the iterator. Will need to perform delete explicitly - if (firstState !== undefined) { - firstState?.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); - numStatesUpdated++; - } - - deleteTimer?.(); - - this.metrics?.regenFnNumStatesUpdated.observe(numStatesUpdated); - } - /** * Get the state to run with `block`. * - State after `block.parentRoot` dialed forward to block.slot diff --git a/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts b/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts index 886f6e386309..f57c9a411923 100644 --- a/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts +++ b/packages/beacon-node/src/chain/stateCache/blockStateCacheImpl.ts @@ -34,7 +34,7 @@ export class BlockStateCacheImpl implements BlockStateCache { this.maxStates = maxStates; this.cache = new MapTracker(metrics?.stateCache); if (metrics) { - this.metrics = {...metrics.stateCache, ...metrics.epochCache}; + this.metrics = metrics.stateCache; metrics.stateCache.size.addCollect(() => metrics.stateCache.size.set(this.cache.size)); } } diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index f572ec0d3c1f..60a49b0b673d 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -124,13 +124,6 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { }), }, - headState: { - unfinalizedPubkeyCacheSize: register.gauge({ - name: "beacon_head_state_unfinalized_pubkey_cache_size", - help: "Current size of the unfinalizedPubkey2Index cache in the head state", - }), - }, - parentBlockDistance: register.histogram({ name: "beacon_imported_block_parent_distance", help: "Histogram of distance to parent block of valid imported blocks", diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index af8d87daa246..461f9c9e935b 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -378,17 +378,6 @@ export function createLodestarMetrics( help: "Total count state.validators nodesPopulated is false on stfn for post state", }), - epochCache: { - finalizedPubkeyDuplicateInsert: register.gauge({ - name: "lodestar_epoch_cache_finalized_pubkey_duplicate_insert_total", - help: "Total count of duplicate insert of finalized pubkeys", - }), - newUnFinalizedPubkey: register.gauge({ - name: "lodestar_epoch_cache_new_unfinalized_pubkey_total", - help: "Total count of unfinalized pubkeys added", - }), - }, - // BLS verifier thread pool and queue bls: { diff --git a/packages/beacon-node/test/memory/unfinalizedPubkey2Index.ts b/packages/beacon-node/test/memory/unfinalizedPubkey2Index.ts deleted file mode 100644 index 294dde750865..000000000000 --- a/packages/beacon-node/test/memory/unfinalizedPubkey2Index.ts +++ /dev/null @@ -1,54 +0,0 @@ -import crypto from "node:crypto"; -import {toMemoryEfficientHexStr} from "@lodestar/state-transition/src/cache/pubkeyCache.js"; -import {ValidatorIndex} from "@lodestar/types"; -// biome-ignore lint/suspicious/noShadowRestrictedNames: We explicitly want `Map` name to be imported -import {Map} from "immutable"; -import {testRunnerMemory} from "./testRunnerMemory.js"; - -// Results in MacOS Nov 2023 -// -// UnfinalizedPubkey2Index 1000 keys - 274956.5 bytes / instance -// UnfinalizedPubkey2Index 10000 keys - 2591129.3 bytes / instance -// UnfinalizedPubkey2Index 100000 keys - 27261443.4 bytes / instance - -testRunnerMemoryBpi([ - { - id: "UnfinalizedPubkey2Index 1000 keys", - getInstance: () => getRandomMap(1000, () => toMemoryEfficientHexStr(crypto.randomBytes(48))), - }, - { - id: "UnfinalizedPubkey2Index 10000 keys", - getInstance: () => getRandomMap(10000, () => toMemoryEfficientHexStr(crypto.randomBytes(48))), - }, - { - id: "UnfinalizedPubkey2Index 100000 keys", - getInstance: () => getRandomMap(100000, () => toMemoryEfficientHexStr(crypto.randomBytes(48))), - }, -]); - -function getRandomMap(n: number, getKey: (i: number) => string): Map { - const map = Map(); - - return map.withMutations((m) => { - for (let i = 0; i < n; i++) { - m.set(getKey(i), i); - } - }); -} - -/** - * Test bytes per instance in different representations of raw binary data - */ -function testRunnerMemoryBpi(testCases: {getInstance: (bytes: number) => unknown; id: string}[]): void { - const longestId = Math.max(...testCases.map(({id}) => id.length)); - - for (const {id, getInstance} of testCases) { - const bpi = testRunnerMemory({ - getInstance, - convergeFactor: 1 / 100, - sampleEvery: 5, - }); - - console.log(`${id.padEnd(longestId)} - ${bpi.toFixed(1)} bytes / instance`); - } -} diff --git a/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts b/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts deleted file mode 100644 index 8009f36c6301..000000000000 --- a/packages/beacon-node/test/perf/chain/stateCache/updateUnfinalizedPubkeys.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import {digest} from "@chainsafe/as-sha256"; -import {SecretKey} from "@chainsafe/blst"; -import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; -import {itBench, setBenchOpts} from "@dapplion/benchmark"; -import {type CachedBeaconStateAllForks, toMemoryEfficientHexStr} from "@lodestar/state-transition"; -import {ValidatorIndex, ssz} from "@lodestar/types"; -import {bytesToBigInt, intToBytes} from "@lodestar/utils"; -import {toBufferBE} from "bigint-buffer"; -import {Map as ImmutableMap} from "immutable"; -import {BlockStateCacheImpl, InMemoryCheckpointStateCache} from "../../../../src/chain/stateCache/index.js"; -import {BlockStateCache} from "../../../../src/chain/stateCache/types.js"; -import {generateCachedElectraState} from "../../../utils/state.js"; - -// Benchmark date from Mon Nov 21 2023 - Intel Core i7-9750H @ 2.60Ghz -// ✔ updateUnfinalizedPubkeys - updating 10 pubkeys 1444.173 ops/s 692.4380 us/op - 1057 runs 6.03 s -// ✔ updateUnfinalizedPubkeys - updating 100 pubkeys 189.5965 ops/s 5.274358 ms/op - 57 runs 1.15 s -// ✔ updateUnfinalizedPubkeys - updating 1000 pubkeys 12.90495 ops/s 77.48967 ms/op - 13 runs 1.62 s -describe("updateUnfinalizedPubkeys perf tests", () => { - setBenchOpts({noThreshold: true}); - - const numPubkeysToBeFinalizedCases = [10, 100, 1000]; - const numCheckpointStateCache = 8; - const numStateCache = 3 * 32; - - let checkpointStateCache: InMemoryCheckpointStateCache; - let stateCache: BlockStateCache; - - const unfinalizedPubkey2Index = generatePubkey2Index(0, Math.max.apply(null, numPubkeysToBeFinalizedCases)); - const baseState = generateCachedElectraState(); - - for (const numPubkeysToBeFinalized of numPubkeysToBeFinalizedCases) { - itBench({ - id: `updateUnfinalizedPubkeys - updating ${numPubkeysToBeFinalized} pubkeys`, - beforeEach: async () => { - baseState.epochCtx.unfinalizedPubkey2index = ImmutableMap(unfinalizedPubkey2Index); - baseState.epochCtx.pubkey2index = new PubkeyIndexMap(); - baseState.epochCtx.index2pubkey = []; - - checkpointStateCache = new InMemoryCheckpointStateCache({}); - stateCache = new BlockStateCacheImpl({}); - - for (let i = 0; i < numCheckpointStateCache; i++) { - const clonedState = baseState.clone(); - const checkpoint = ssz.phase0.Checkpoint.defaultValue(); - - clonedState.slot = i; - checkpoint.epoch = i; // Assigning arbitrary non-duplicate values to ensure checkpointStateCache correctly saves all the states - - checkpointStateCache.add(checkpoint, clonedState); - } - - for (let i = 0; i < numStateCache; i++) { - const clonedState = baseState.clone(); - clonedState.slot = i; - stateCache.add(clonedState); - } - }, - fn: async () => { - const newFinalizedValidators = baseState.epochCtx.unfinalizedPubkey2index.filter( - (index, _pubkey) => index < numPubkeysToBeFinalized - ); - - const states = stateCache.getStates(); - const cpStates = checkpointStateCache.getStates(); - - const firstState = states.next().value as CachedBeaconStateAllForks; - firstState.epochCtx.addFinalizedPubkeys(newFinalizedValidators); - - const pubkeysToDelete = Array.from(newFinalizedValidators.keys()); - - firstState.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); - - for (const s of states) { - s.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); - } - - for (const s of cpStates) { - s.epochCtx.deleteUnfinalizedPubkeys(pubkeysToDelete); - } - }, - }); - } - - type PubkeyHex = string; - - function generatePubkey2Index(startIndex: number, endIndex: number): Map { - const pubkey2Index = new Map(); - const pubkeys = generatePubkeys(endIndex - startIndex); - - for (let i = startIndex; i < endIndex; i++) { - pubkey2Index.set(toMemoryEfficientHexStr(pubkeys[i]), i); - } - - return pubkey2Index; - } - - function generatePubkeys(validatorCount: number): Uint8Array[] { - const keys = []; - - for (let i = 0; i < validatorCount; i++) { - const sk = generatePrivateKey(i); - const pk = sk.toPublicKey().toBytes(); - keys.push(pk); - } - - return keys; - } - - function generatePrivateKey(index: number): SecretKey { - const secretKeyBytes = toBufferBE(bytesToBigInt(digest(intToBytes(index, 32))) % BigInt("38581184513"), 32); - const secret: SecretKey = SecretKey.fromBytes(secretKeyBytes); - return secret; - } -}); diff --git a/packages/beacon-node/test/sim/electra-interop.test.ts b/packages/beacon-node/test/sim/electra-interop.test.ts index 47b7d127fb42..148f210f7f65 100644 --- a/packages/beacon-node/test/sim/electra-interop.test.ts +++ b/packages/beacon-node/test/sim/electra-interop.test.ts @@ -375,17 +375,8 @@ describe("executionEngine / ExecutionEngineHttp", () => { if (headState.validators.length !== 33 || headState.balances.length !== 33) { throw Error("New validator is not reflected in the beacon state at slot 5"); } - if (epochCtx.index2pubkey.length !== 32 || epochCtx.pubkey2index.size !== 32) { - throw Error("Finalized cache is modified."); - } - if (epochCtx.unfinalizedPubkey2index.size !== 1) { - throw Error( - `Unfinalized cache is missing the expected validator. Size: ${epochCtx.unfinalizedPubkey2index.size}` - ); - } - // validator count at epoch 1 should be empty at this point since no epoch transition has happened. - if (epochCtx.getValidatorCountAtEpoch(1) !== undefined) { - throw Error("Historical validator lengths is modified"); + if (epochCtx.index2pubkey.length !== 33 || epochCtx.pubkey2index.size !== 33) { + throw Error("Pubkey cache is not updated"); } await new Promise((resolve, _reject) => { @@ -412,23 +403,7 @@ describe("executionEngine / ExecutionEngineHttp", () => { throw Error("New validator is not reflected in the beacon state."); } if (epochCtx.index2pubkey.length !== 33 || epochCtx.pubkey2index.size !== 33) { - throw Error("New validator is not in finalized cache"); - } - if (!epochCtx.unfinalizedPubkey2index.isEmpty()) { - throw Error("Unfinalized cache still contains new validator"); - } - // After 4 epochs, headState's finalized cp epoch should be 2 - // epochCtx should only have validator count for epoch 3 and 4. - if (epochCtx.getValidatorCountAtEpoch(4) === undefined || epochCtx.getValidatorCountAtEpoch(3) === undefined) { - throw Error("Missing historical validator length for epoch 3 or 4"); - } - - if (epochCtx.getValidatorCountAtEpoch(4) !== 33 || epochCtx.getValidatorCountAtEpoch(3) !== 33) { - throw Error("Incorrect historical validator length for epoch 3 or 4"); - } - - if (epochCtx.getValidatorCountAtEpoch(2) !== undefined || epochCtx.getValidatorCountAtEpoch(1) !== undefined) { - throw Error("Historical validator length for epoch 1 or 2 is not dropped properly"); + throw Error("New validator is not in pubkey cache"); } if (headState.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) { diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 5dfecfa9bbf6..ac297b240756 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -69,8 +69,7 @@ "@lodestar/params": "^1.23.0", "@lodestar/types": "^1.23.0", "@lodestar/utils": "^1.23.0", - "bigint-buffer": "^1.1.5", - "immutable": "^4.3.2" + "bigint-buffer": "^1.1.5" }, "keywords": [ "ethereum", diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 207267dff4f4..86e63c672024 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -25,10 +25,8 @@ import { electra, phase0, } from "@lodestar/types"; -import {LodestarError, fromHex} from "@lodestar/utils"; -import * as immutable from "immutable"; +import {LodestarError} from "@lodestar/utils"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; -import {EpochCacheMetrics} from "../metrics.js"; import {AttesterDuty, calculateCommitteeAssignments} from "../util/calculateCommitteeAssignments.js"; import { EpochShuffling, @@ -51,14 +49,7 @@ import { import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js"; import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js"; import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js"; -import { - Index2PubkeyCache, - PubkeyHex, - UnfinalizedPubkeyIndexMap, - newUnfinalizedPubkeyIndexMap, - syncPubkeys, - toMemoryEfficientHexStr, -} from "./pubkeyCache.js"; +import {Index2PubkeyCache, syncPubkeys} from "./pubkeyCache.js"; import {CachedBeaconStateAllForks} from "./stateCache.js"; import { SyncCommitteeCache, @@ -111,30 +102,20 @@ type ProposersDeferred = {computed: false; seed: Uint8Array} | {computed: true; export class EpochCache { config: BeaconConfig; /** - * Unique globally shared finalized pubkey registry. There should only exist one for the entire application. + * Unique globally shared pubkey registry. There should only exist one for the entire application. * * TODO: this is a hack, we need a safety mechanism in case a bad eth1 majority vote is in, * or handle non finalized data differently, or use an immutable.js structure for cheap copies * - * New: This would include only validators whose activation_eligibility_epoch != FAR_FUTURE_EPOCH and hence it is - * insert only. Validators could be 1) Active 2) In the activation queue 3) Initialized but pending queued - * * $VALIDATOR_COUNT x 192 char String -> Number Map */ pubkey2index: PubkeyIndexMap; /** - * Unique globally shared finalized pubkey registry. There should only exist one for the entire application. - * - * New: This would include only validators whose activation_eligibility_epoch != FAR_FUTURE_EPOCH and hence it is - * insert only. Validators could be 1) Active 2) In the activation queue 3) Initialized but pending queued + * Unique globally shared pubkey registry. There should only exist one for the entire application. * * $VALIDATOR_COUNT x BLST deserialized pubkey (Jacobian coordinates) */ index2pubkey: Index2PubkeyCache; - /** - * Unique pubkey registry shared in the same fork. There should only exist one for the fork. - */ - unfinalizedPubkey2index: UnfinalizedPubkeyIndexMap; /** * ShufflingCache is passed in from `beacon-node` so should be available at runtime but may not be * present during testing. @@ -252,15 +233,6 @@ export class EpochCache { // TODO: Helper stats syncPeriod: SyncPeriod; - /** - * state.validators.length of every state at epoch boundary - * They are saved in increasing order of epoch. - * The first validator length in the list corresponds to the state AFTER the latest finalized checkpoint state. ie. state.finalizedCheckpoint.epoch - 1 - * The last validator length corresponds to the latest epoch state ie. this.epoch - * eg. latest epoch = 105, latest finalized cp state epoch = 102 - * then the list will be (in terms of epoch) [103, 104, 105] - */ - historicalValidatorLengths: immutable.List; epoch: Epoch; @@ -272,7 +244,6 @@ export class EpochCache { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; - unfinalizedPubkey2index: UnfinalizedPubkeyIndexMap; shufflingCache?: IShufflingCache; proposers: number[]; proposersPrevEpoch: number[] | null; @@ -300,12 +271,10 @@ export class EpochCache { nextSyncCommitteeIndexed: SyncCommitteeCache; epoch: Epoch; syncPeriod: SyncPeriod; - historialValidatorLengths: immutable.List; }) { this.config = data.config; this.pubkey2index = data.pubkey2index; this.index2pubkey = data.index2pubkey; - this.unfinalizedPubkey2index = data.unfinalizedPubkey2index; this.shufflingCache = data.shufflingCache; this.proposers = data.proposers; this.proposersPrevEpoch = data.proposersPrevEpoch; @@ -333,12 +302,11 @@ export class EpochCache { this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed; this.epoch = data.epoch; this.syncPeriod = data.syncPeriod; - this.historicalValidatorLengths = data.historialValidatorLengths; } /** * Create an epoch cache - * @param state a finalized beacon state. Passing in unfinalized state may cause unexpected behaviour eg. empty unfinalized cache + * @param state a finalized beacon state. Passing in unfinalized state may cause unexpected behaviour * * SLOW CODE - 🐢 */ @@ -551,8 +519,6 @@ export class EpochCache { config, pubkey2index, index2pubkey, - // `createFromFinalizedState()` creates cache with empty unfinalizedPubkey2index. Be cautious to only pass in finalized state - unfinalizedPubkey2index: newUnfinalizedPubkeyIndexMap(), shufflingCache, proposers, // On first epoch, set to null to prevent unnecessary work since this is only used for metrics @@ -581,7 +547,6 @@ export class EpochCache { nextSyncCommitteeIndexed, epoch: currentEpoch, syncPeriod: computeSyncPeriodAtEpoch(currentEpoch), - historialValidatorLengths: immutable.List(), }); } @@ -597,8 +562,6 @@ export class EpochCache { // Common append-only structures shared with all states, no need to clone pubkey2index: this.pubkey2index, index2pubkey: this.index2pubkey, - // No need to clone this reference. On each mutation the `unfinalizedPubkey2index` reference is replaced, @see `addPubkey` - unfinalizedPubkey2index: this.unfinalizedPubkey2index, shufflingCache: this.shufflingCache, // Immutable data proposers: this.proposers, @@ -630,7 +593,6 @@ export class EpochCache { nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed, epoch: this.epoch, syncPeriod: this.syncPeriod, - historialValidatorLengths: this.historicalValidatorLengths, }); } @@ -773,25 +735,6 @@ export class EpochCache { // ``` this.epoch = computeEpochAtSlot(state.slot); this.syncPeriod = computeSyncPeriodAtEpoch(this.epoch); - // ELECTRA Only: Add current cpState.validators.length - // Only keep validatorLength for epochs after finalized cpState.epoch - // eg. [100(epoch 1), 102(epoch 2)].push(104(epoch 3)), this.epoch = 3, finalized cp epoch = 1 - // We keep the last (3 - 1) items = [102, 104] - if (upcomingEpoch >= this.config.ELECTRA_FORK_EPOCH) { - this.historicalValidatorLengths = this.historicalValidatorLengths.push(state.validators.length); - - // If number of validatorLengths we want to keep exceeds the current list size, it implies - // finalized checkpoint hasn't advanced, and no need to slice - const hasFinalizedCpAdvanced = - this.epoch - state.finalizedCheckpoint.epoch < this.historicalValidatorLengths.size; - - if (hasFinalizedCpAdvanced) { - // We use finalized cp epoch - this.epoch which is a negative number to keep the last n entries and discard the rest - this.historicalValidatorLengths = this.historicalValidatorLengths.slice( - state.finalizedCheckpoint.epoch - this.epoch - ); - } - } } beforeEpochTransition(): void { @@ -1018,75 +961,19 @@ export class EpochCache { } /** - * Return finalized pubkey given the validator index. - * Only finalized pubkey as we do not store unfinalized pubkey because no where in the spec has a - * need to make such enquiry + * Return pubkey given the validator index. */ getPubkey(index: ValidatorIndex): PublicKey | undefined { return this.index2pubkey[index]; } getValidatorIndex(pubkey: Uint8Array): ValidatorIndex | null { - if (this.isPostElectra()) { - return this.pubkey2index.get(pubkey) ?? this.unfinalizedPubkey2index.get(toMemoryEfficientHexStr(pubkey)) ?? null; - } return this.pubkey2index.get(pubkey); } - /** - * - * Add unfinalized pubkeys - * - */ addPubkey(index: ValidatorIndex, pubkey: Uint8Array): void { - if (this.isPostElectra()) { - this.addUnFinalizedPubkey(index, pubkey); - } else { - // deposit mechanism pre ELECTRA follows a safe distance with assumption - // that they are already canonical - this.addFinalizedPubkey(index, pubkey); - } - } - - addUnFinalizedPubkey(index: ValidatorIndex, pubkey: PubkeyHex | Uint8Array, metrics?: EpochCacheMetrics): void { - this.unfinalizedPubkey2index = this.unfinalizedPubkey2index.set(toMemoryEfficientHexStr(pubkey), index); - metrics?.newUnFinalizedPubkey.inc(); - } - - addFinalizedPubkeys(pubkeyMap: UnfinalizedPubkeyIndexMap, metrics?: EpochCacheMetrics): void { - pubkeyMap.forEach((index, pubkey) => this.addFinalizedPubkey(index, pubkey, metrics)); - } - - /** - * Add finalized validator index and pubkey into finalized cache. - * Since addFinalizedPubkey() primarily takes pubkeys from unfinalized cache, it can take pubkey hex string directly - */ - addFinalizedPubkey(index: ValidatorIndex, pubkeyOrHex: PubkeyHex | Uint8Array, metrics?: EpochCacheMetrics): void { - const pubkey = typeof pubkeyOrHex === "string" ? fromHex(pubkeyOrHex) : pubkeyOrHex; - const existingIndex = this.pubkey2index.get(pubkey); - - if (existingIndex !== null) { - if (existingIndex === index) { - // Repeated insert. - metrics?.finalizedPubkeyDuplicateInsert.inc(); - return; - } - // attempt to insert the same pubkey with different index, should never happen. - throw Error( - `inserted existing pubkey into finalizedPubkey2index cache with a different index, index=${index} priorIndex=${existingIndex}` - ); - } - this.pubkey2index.set(pubkey, index); - const pubkeyBytes = pubkey instanceof Uint8Array ? pubkey : fromHex(pubkey); - this.index2pubkey[index] = PublicKey.fromBytes(pubkeyBytes); // Optimize for aggregation - } - - /** - * Delete pubkeys from unfinalized cache - */ - deleteUnfinalizedPubkeys(pubkeys: Iterable): void { - this.unfinalizedPubkey2index = this.unfinalizedPubkey2index.deleteAll(pubkeys); + this.index2pubkey[index] = PublicKey.fromBytes(pubkey); // Optimize for aggregation } getShufflingAtSlot(slot: Slot): EpochShuffling { @@ -1220,25 +1107,6 @@ export class EpochCache { isPostElectra(): boolean { return this.epoch >= this.config.ELECTRA_FORK_EPOCH; } - - getValidatorCountAtEpoch(targetEpoch: Epoch): number | undefined { - const currentEpoch = this.epoch; - - if (targetEpoch === currentEpoch) { - return this.historicalValidatorLengths.get(-1); - } - - // Attempt to get validator count from future epoch - if (targetEpoch > currentEpoch) { - return undefined; - } - - // targetEpoch is so far back that historicalValidatorLengths doesnt contain such info - if (targetEpoch < currentEpoch - this.historicalValidatorLengths.size + 1) { - return undefined; - } - return this.historicalValidatorLengths.get(targetEpoch - currentEpoch - 1); - } } function getEffectiveBalanceIncrementsByteLen(validatorCount: number): number { diff --git a/packages/state-transition/src/cache/pubkeyCache.ts b/packages/state-transition/src/cache/pubkeyCache.ts index 16c8f3de6787..75281e52e060 100644 --- a/packages/state-transition/src/cache/pubkeyCache.ts +++ b/packages/state-transition/src/cache/pubkeyCache.ts @@ -1,44 +1,8 @@ import {PublicKey} from "@chainsafe/blst"; import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {ValidatorIndex, phase0} from "@lodestar/types"; -import * as immutable from "immutable"; export type Index2PubkeyCache = PublicKey[]; -/** - * OrderedMap preserves the order of entries in which they are `set()`. - * We assume `values()` yields validator indices in strictly increasing order - * as new validator indices are assigned in increasing order. - * EIP-6914 will break this assumption. - */ -export type UnfinalizedPubkeyIndexMap = immutable.Map; - -export type PubkeyHex = string; - -/** - * toHexString() creates hex strings via string concatenation, which are very memory inefficient. - * Memory benchmarks show that Buffer.toString("hex") produces strings with 10x less memory. - * - * Does not prefix to save memory, thus the prefix is removed from an already string representation. - * - * See https://github.com/ChainSafe/lodestar/issues/3446 - */ -export function toMemoryEfficientHexStr(hex: Uint8Array | string): string { - if (typeof hex === "string") { - if (hex.startsWith("0x")) { - hex = hex.slice(2); - } - return hex; - } - - return Buffer.from(hex.buffer, hex.byteOffset, hex.byteLength).toString("hex"); -} - -/** - * A wrapper for calling immutable.js. To abstract the initialization of UnfinalizedPubkeyIndexMap - */ -export function newUnfinalizedPubkeyIndexMap(): UnfinalizedPubkeyIndexMap { - return immutable.Map(); -} /** * Checks the pubkey indices against a state and adds missing pubkeys diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 600bbf173462..0e76c5248a97 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -41,15 +41,10 @@ export { EpochCacheError, EpochCacheErrorCode, } from "./cache/epochCache.js"; -export {toMemoryEfficientHexStr} from "./cache/pubkeyCache.js"; export {type EpochTransitionCache, beforeProcessEpoch} from "./cache/epochTransitionCache.js"; // Aux data-structures -export { - type Index2PubkeyCache, - type UnfinalizedPubkeyIndexMap, - newUnfinalizedPubkeyIndexMap, -} from "./cache/pubkeyCache.js"; +export {type Index2PubkeyCache} from "./cache/pubkeyCache.js"; export { type EffectiveBalanceIncrements, diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index fee20de1d565..d975833ba13c 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -31,11 +31,6 @@ export type BeaconStateTransitionMetrics = { ) => void; }; -export type EpochCacheMetrics = { - finalizedPubkeyDuplicateInsert: Gauge; - newUnFinalizedPubkey: Gauge; -}; - export function onStateCloneMetrics( state: CachedBeaconStateAllForks, metrics: BeaconStateTransitionMetrics, diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index de2ba5893b02..9466dfd83ab7 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -28,7 +28,7 @@ describe("CachedBeaconState", () => { expect(state2.epochCtx.epoch).toBe(0); }); - it("Clone and mutate cache pre-Electra", () => { + it("Clone and mutate cache", () => { const stateView = ssz.altair.BeaconState.defaultViewDU(); const state1 = createCachedBeaconStateTest(stateView); @@ -52,40 +52,6 @@ describe("CachedBeaconState", () => { expect(state2.epochCtx.getValidatorIndex(pubkey2)).toBe(index2); }); - it("Clone and mutate cache post-Electra", () => { - const stateView = ssz.electra.BeaconState.defaultViewDU(); - const state1 = createCachedBeaconStateTest( - stateView, - createChainForkConfig({ - ALTAIR_FORK_EPOCH: 0, - BELLATRIX_FORK_EPOCH: 0, - CAPELLA_FORK_EPOCH: 0, - DENEB_FORK_EPOCH: 0, - ELECTRA_FORK_EPOCH: 0, - }), - {skipSyncCommitteeCache: true, skipSyncPubkeys: true} - ); - - const pubkey1 = fromHexString( - "0x84105a985058fc8740a48bf1ede9d223ef09e8c6b1735ba0a55cf4a9ff2ff92376b778798365e488dab07a652eb04576" - ); - const index1 = 123; - const pubkey2 = fromHexString( - "0xa41726266b1d83ef609d759ba7796d54cfe549154e01e4730a3378309bc81a7638140d7e184b33593c072595f23f032d" - ); - const index2 = 456; - - state1.epochCtx.addPubkey(index1, pubkey1); - - const state2 = state1.clone(); - state2.epochCtx.addPubkey(index2, pubkey2); - - expect(state1.epochCtx.getValidatorIndex(pubkey1)).toBe(index1); - expect(state2.epochCtx.getValidatorIndex(pubkey1)).toBe(index1); - expect(state1.epochCtx.getValidatorIndex(pubkey2)).toBe(null); - expect(state2.epochCtx.getValidatorIndex(pubkey2)).toBe(index2); - }); - it("Auto-commit on hashTreeRoot", () => { // Use Checkpoint instead of BeaconState to speed up the test const cp1 = ssz.phase0.Checkpoint.defaultViewDU(); diff --git a/yarn.lock b/yarn.lock index a9cb9ebe4fdc..f59b3d082456 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7267,11 +7267,6 @@ ignore@^5.0.4, ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== -immutable@^4.3.2: - version "4.3.5" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.5.tgz#f8b436e66d59f99760dc577f5c99a4fd2a5cc5a0" - integrity sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw== - import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz"