From d47774ff0db29d1d5f0e5fded47393df4e28b86d Mon Sep 17 00:00:00 2001 From: Cayman Date: Wed, 10 Jul 2024 02:20:23 -0400 Subject: [PATCH 01/11] chore: separate epoch transition cache flags --- .../src/chain/rewards/attestationsRewards.ts | 14 ++++----- .../src/metrics/validatorMonitor.ts | 15 ++++++--- .../src/cache/epochTransitionCache.ts | 31 ++++++++++++------- .../src/epoch/getAttestationDeltas.ts | 19 ++++++------ .../src/epoch/getRewardsAndPenalties.ts | 18 +++++------ .../src/epoch/processInactivityUpdates.ts | 8 ++--- .../src/epoch/processPendingAttestations.ts | 10 +++--- packages/state-transition/src/metrics.ts | 7 ++++- .../state-transition/src/stateTransition.ts | 4 +-- .../src/util/attesterStatus.ts | 2 -- .../test/perf/analyzeEpochs.ts | 4 +-- .../test/perf/epoch/utilPhase0.ts | 16 +++++----- 12 files changed, 84 insertions(+), 64 deletions(-) diff --git a/packages/beacon-node/src/chain/rewards/attestationsRewards.ts b/packages/beacon-node/src/chain/rewards/attestationsRewards.ts index 3b4583826349..e909e4b1b57e 100644 --- a/packages/beacon-node/src/chain/rewards/attestationsRewards.ts +++ b/packages/beacon-node/src/chain/rewards/attestationsRewards.ts @@ -140,7 +140,7 @@ function computeTotalAttestationsRewardsAltair( validatorIds: (ValidatorIndex | string)[] = [] ): TotalAttestationsReward[] { const rewards = []; - const {statuses} = transitionCache; + const {flags} = transitionCache; const {epochCtx, config} = state; const validatorIndices = validatorIds .map((id) => (typeof id === "number" ? id : epochCtx.pubkey2index.get(id))) @@ -148,13 +148,13 @@ function computeTotalAttestationsRewardsAltair( const inactivityPenaltyDenominator = config.INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT_ALTAIR; - for (let i = 0; i < statuses.length; i++) { + for (let i = 0; i < flags.length; i++) { if (validatorIndices.length && !validatorIndices.includes(i)) { continue; } - const status = statuses[i]; - if (!hasMarkers(status.flags, FLAG_ELIGIBLE_ATTESTER)) { + const flag = flags[i]; + if (!hasMarkers(flag, FLAG_ELIGIBLE_ATTESTER)) { continue; } @@ -162,13 +162,13 @@ function computeTotalAttestationsRewardsAltair( const currentRewards = {...defaultAttestationsReward, validatorIndex: i}; - if (hasMarkers(status.flags, FLAG_PREV_SOURCE_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_SOURCE_ATTESTER_UNSLASHED)) { currentRewards.source = idealRewards[effectiveBalanceIncrement].source; } else { currentRewards.source = penalties[effectiveBalanceIncrement].source * -1; // Negative reward to indicate penalty } - if (hasMarkers(status.flags, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { currentRewards.target = idealRewards[effectiveBalanceIncrement].target; } else { currentRewards.target = penalties[effectiveBalanceIncrement].target * -1; @@ -179,7 +179,7 @@ function computeTotalAttestationsRewardsAltair( currentRewards.inactivity = Math.floor(inactivityPenaltyNumerator / inactivityPenaltyDenominator) * -1; } - if (hasMarkers(status.flags, FLAG_PREV_HEAD_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_HEAD_ATTESTER_UNSLASHED)) { currentRewards.head = idealRewards[effectiveBalanceIncrement].head; } diff --git a/packages/beacon-node/src/metrics/validatorMonitor.ts b/packages/beacon-node/src/metrics/validatorMonitor.ts index a9d783786e88..df6680627f33 100644 --- a/packages/beacon-node/src/metrics/validatorMonitor.ts +++ b/packages/beacon-node/src/metrics/validatorMonitor.ts @@ -39,7 +39,12 @@ export enum OpSource { export type ValidatorMonitor = { registerLocalValidator(index: number): void; registerLocalValidatorInSyncCommittee(index: number, untilEpoch: Epoch): void; - registerValidatorStatuses(currentEpoch: Epoch, statuses: AttesterStatus[], balances?: number[]): void; + registerValidatorStatuses( + currentEpoch: Epoch, + statuses: AttesterStatus[], + flags: Uint8Array, + balances?: number[] + ): void; registerBeaconBlock(src: OpSource, seenTimestampSec: Seconds, block: BeaconBlock): void; registerBlobSidecar(src: OpSource, seenTimestampSec: Seconds, blob: deneb.BlobSidecar): void; registerImportedBlock(block: BeaconBlock, data: {proposerBalanceDelta: number}): void; @@ -115,8 +120,8 @@ type ValidatorStatus = { inclusionDistance: number; }; -function statusToSummary(status: AttesterStatus): ValidatorStatus { - const flags = parseAttesterFlags(status.flags); +function statusToSummary(status: AttesterStatus, flag: number): ValidatorStatus { + const flags = parseAttesterFlags(flag); return { isSlashed: flags.unslashed, isActiveInCurrentEpoch: status.active, @@ -287,7 +292,7 @@ export function createValidatorMonitor( } }, - registerValidatorStatuses(currentEpoch, statuses, balances) { + registerValidatorStatuses(currentEpoch, statuses, flags, balances) { // Prevent registering status for the same epoch twice. processEpoch() may be ran more than once for the same epoch. if (currentEpoch <= lastRegisteredStatusEpoch) { return; @@ -306,7 +311,7 @@ export function createValidatorMonitor( continue; } - const summary = statusToSummary(status); + const summary = statusToSummary(status, flags[index]); if (summary.isPrevSourceAttester) { metrics.validatorMonitor.prevEpochOnChainSourceAttesterHit.inc(); diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index dc4edf26e084..491f75f307f0 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -130,6 +130,8 @@ export interface EpochTransitionCache { */ statuses: AttesterStatus[]; + flags: Uint8Array; + /** * balances array will be populated by processRewardsAndPenalties() and consumed by processEffectiveBalanceUpdates(). * processRewardsAndPenalties() already has a regular Javascript array of balances. @@ -200,6 +202,11 @@ export function beforeProcessEpoch( const validators = state.validators.getAllReadonlyValues(); const validatorCount = validators.length; + // Flags for each validator + // We can use Uint8Array since we only need 8 bits per validator + // See src/util/attesterStatus.ts + const flags = new Uint8Array(validatorCount); + // Clone before being mutated in processEffectiveBalanceUpdates epochCtx.beforeEpochTransition(); @@ -214,7 +221,7 @@ export function beforeProcessEpoch( indicesToSlash.push(i); } } else { - status.flags |= FLAG_UNSLASHED; + flags[i] |= FLAG_UNSLASHED; } const {activationEpoch, exitEpoch} = validator; @@ -230,7 +237,7 @@ export function beforeProcessEpoch( // TODO: Consider using an array of `eligibleValidatorIndices: number[]` if (isActivePrev || (validator.slashed && prevEpoch + 1 < validator.withdrawableEpoch)) { eligibleValidatorIndices.push(i); - status.flags |= FLAG_ELIGIBLE_ATTESTER; + flags[i] |= FLAG_ELIGIBLE_ATTESTER; } if (isActiveCurr) { @@ -313,6 +320,7 @@ export function beforeProcessEpoch( processPendingAttestations( state as CachedBeaconStatePhase0, statuses, + flags, (state as CachedBeaconStatePhase0).previousEpochAttestations.getAllReadonly(), prevEpoch, FLAG_PREV_SOURCE_ATTESTER, @@ -322,6 +330,7 @@ export function beforeProcessEpoch( processPendingAttestations( state as CachedBeaconStatePhase0, statuses, + flags, (state as CachedBeaconStatePhase0).currentEpochAttestations.getAllReadonly(), currentEpoch, FLAG_CURR_SOURCE_ATTESTER, @@ -331,11 +340,10 @@ export function beforeProcessEpoch( } else { const previousEpochParticipation = (state as CachedBeaconStateAltair).previousEpochParticipation.getAll(); for (let i = 0; i < previousEpochParticipation.length; i++) { - const status = statuses[i]; // this is required to pass random spec tests in altair if (isActivePrevEpoch[i]) { // FLAG_PREV are indexes [0,1,2] - status.flags |= previousEpochParticipation[i]; + flags[i] |= previousEpochParticipation[i]; } } @@ -345,7 +353,7 @@ export function beforeProcessEpoch( // this is required to pass random spec tests in altair if (status.active) { // FLAG_PREV are indexes [3,4,5], so shift by 3 - status.flags |= currentEpochParticipation[i] << 3; + flags[i] |= currentEpochParticipation[i] << 3; } } } @@ -361,19 +369,19 @@ export function beforeProcessEpoch( const FLAG_PREV_HEAD_ATTESTER_UNSLASHED = FLAG_PREV_HEAD_ATTESTER | FLAG_UNSLASHED; const FLAG_CURR_TARGET_UNSLASHED = FLAG_CURR_TARGET_ATTESTER | FLAG_UNSLASHED; - for (let i = 0; i < statuses.length; i++) { - const status = statuses[i]; + for (let i = 0; i < validatorCount; i++) { const effectiveBalanceByIncrement = effectiveBalancesByIncrements[i]; - if (hasMarkers(status.flags, FLAG_PREV_SOURCE_ATTESTER_UNSLASHED)) { + const flag = flags[i]; + if (hasMarkers(flag, FLAG_PREV_SOURCE_ATTESTER_UNSLASHED)) { prevSourceUnslStake += effectiveBalanceByIncrement; } - if (hasMarkers(status.flags, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { prevTargetUnslStake += effectiveBalanceByIncrement; } - if (hasMarkers(status.flags, FLAG_PREV_HEAD_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_HEAD_ATTESTER_UNSLASHED)) { prevHeadUnslStake += effectiveBalanceByIncrement; } - if (hasMarkers(status.flags, FLAG_CURR_TARGET_UNSLASHED)) { + if (hasMarkers(flag, FLAG_CURR_TARGET_UNSLASHED)) { currTargetUnslStake += effectiveBalanceByIncrement; } } @@ -423,6 +431,7 @@ export function beforeProcessEpoch( nextEpochTotalActiveBalanceByIncrement: 0, isActiveNextEpoch, statuses, + flags, // Will be assigned in processRewardsAndPenalties() balances: undefined, diff --git a/packages/state-transition/src/epoch/getAttestationDeltas.ts b/packages/state-transition/src/epoch/getAttestationDeltas.ts index 94d08b5d8ec6..72f599ebc190 100644 --- a/packages/state-transition/src/epoch/getAttestationDeltas.ts +++ b/packages/state-transition/src/epoch/getAttestationDeltas.ts @@ -52,7 +52,8 @@ export function getAttestationDeltas( state: CachedBeaconStatePhase0, cache: EpochTransitionCache ): [number[], number[]] { - const validatorCount = cache.statuses.length; + const {flags, statuses} = cache; + const validatorCount = flags.length; const rewards = newZeroedArray(validatorCount); const penalties = newZeroedArray(validatorCount); @@ -77,12 +78,12 @@ export function getAttestationDeltas( // effectiveBalance is multiple of EFFECTIVE_BALANCE_INCREMENT and less than MAX_EFFECTIVE_BALANCE // so there are limited values of them like 32, 31, 30 const rewardPnaltyItemCache = new Map(); - const {statuses} = cache; const {effectiveBalanceIncrements} = state.epochCtx; for (let i = 0; i < statuses.length; i++) { + const flag = flags[i]; + const status = statuses[i]; const effectiveBalanceIncrement = effectiveBalanceIncrements[i]; const effectiveBalance = effectiveBalanceIncrement * EFFECTIVE_BALANCE_INCREMENT; - const status = statuses[i]; let rewardItem = rewardPnaltyItemCache.get(effectiveBalanceIncrement); if (!rewardItem) { @@ -121,14 +122,14 @@ export function getAttestationDeltas( } = rewardItem; // inclusion speed bonus - if (hasMarkers(status.flags, FLAG_PREV_SOURCE_ATTESTER_OR_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_SOURCE_ATTESTER_OR_UNSLASHED)) { rewards[status.proposerIndex] += proposerReward; rewards[i] += Math.floor(maxAttesterReward / status.inclusionDelay); } - if (hasMarkers(status.flags, FLAG_ELIGIBLE_ATTESTER)) { + if (hasMarkers(flag, FLAG_ELIGIBLE_ATTESTER)) { // expected FFG source - if (hasMarkers(status.flags, FLAG_PREV_SOURCE_ATTESTER_OR_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_SOURCE_ATTESTER_OR_UNSLASHED)) { // justification-participation reward rewards[i] += sourceCheckpointReward; } else { @@ -137,7 +138,7 @@ export function getAttestationDeltas( } // expected FFG target - if (hasMarkers(status.flags, FLAG_PREV_TARGET_ATTESTER_OR_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_TARGET_ATTESTER_OR_UNSLASHED)) { // boundary-attestation reward rewards[i] += targetCheckpointReward; } else { @@ -146,7 +147,7 @@ export function getAttestationDeltas( } // expected head - if (hasMarkers(status.flags, FLAG_PREV_HEAD_ATTESTER_OR_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_HEAD_ATTESTER_OR_UNSLASHED)) { // canonical-participation reward rewards[i] += headReward; } else { @@ -158,7 +159,7 @@ export function getAttestationDeltas( if (isInInactivityLeak) { penalties[i] += basePenalty; - if (!hasMarkers(status.flags, FLAG_PREV_TARGET_ATTESTER_OR_UNSLASHED)) { + if (!hasMarkers(flag, FLAG_PREV_TARGET_ATTESTER_OR_UNSLASHED)) { penalties[i] += finalityDelayPenalty; } } diff --git a/packages/state-transition/src/epoch/getRewardsAndPenalties.ts b/packages/state-transition/src/epoch/getRewardsAndPenalties.ts index dcffe986e0bb..bf766fe4666a 100644 --- a/packages/state-transition/src/epoch/getRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/getRewardsAndPenalties.ts @@ -29,7 +29,7 @@ type RewardPenaltyItem = { }; /** - * An aggregate of getFlagIndexDeltas and getInactivityPenaltyDeltas that loop through process.statuses 1 time instead of 4. + * An aggregate of getFlagIndexDeltas and getInactivityPenaltyDeltas that loop through process.flags 1 time instead of 4. * * - On normal mainnet conditions * - prevSourceAttester: 98% @@ -62,10 +62,10 @@ export function getRewardsAndPenaltiesAltair( fork === ForkSeq.altair ? INACTIVITY_PENALTY_QUOTIENT_ALTAIR : INACTIVITY_PENALTY_QUOTIENT_BELLATRIX; const penaltyDenominator = config.INACTIVITY_SCORE_BIAS * inactivityPenalityMultiplier; - const {statuses} = cache; - for (let i = 0; i < statuses.length; i++) { - const status = statuses[i]; - if (!hasMarkers(status.flags, FLAG_ELIGIBLE_ATTESTER)) { + const {flags} = cache; + for (let i = 0; i < flags.length; i++) { + const flag = flags[i]; + if (!hasMarkers(flag, FLAG_ELIGIBLE_ATTESTER)) { continue; } @@ -98,7 +98,7 @@ export function getRewardsAndPenaltiesAltair( rewardPenaltyItem; // same logic to getFlagIndexDeltas - if (hasMarkers(status.flags, FLAG_PREV_SOURCE_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_SOURCE_ATTESTER_UNSLASHED)) { if (!isInInactivityLeakBn) { rewards[i] += timelySourceReward; } @@ -106,7 +106,7 @@ export function getRewardsAndPenaltiesAltair( penalties[i] += timelySourcePenalty; } - if (hasMarkers(status.flags, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { if (!isInInactivityLeakBn) { rewards[i] += timelyTargetReward; } @@ -114,7 +114,7 @@ export function getRewardsAndPenaltiesAltair( penalties[i] += timelyTargetPenalty; } - if (hasMarkers(status.flags, FLAG_PREV_HEAD_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_HEAD_ATTESTER_UNSLASHED)) { if (!isInInactivityLeakBn) { rewards[i] += timelyHeadReward; } @@ -122,7 +122,7 @@ export function getRewardsAndPenaltiesAltair( // Same logic to getInactivityPenaltyDeltas // TODO: if we have limited value in inactivityScores we can provide a cache too - if (!hasMarkers(status.flags, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { + if (!hasMarkers(flag, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { const penaltyNumerator = effectiveBalanceIncrement * EFFECTIVE_BALANCE_INCREMENT * state.inactivityScores.get(i); penalties[i] += Math.floor(penaltyNumerator / penaltyDenominator); } diff --git a/packages/state-transition/src/epoch/processInactivityUpdates.ts b/packages/state-transition/src/epoch/processInactivityUpdates.ts index 5a84f9a48b66..aedb077d6bbe 100644 --- a/packages/state-transition/src/epoch/processInactivityUpdates.ts +++ b/packages/state-transition/src/epoch/processInactivityUpdates.ts @@ -4,7 +4,7 @@ import * as attesterStatusUtil from "../util/attesterStatus.js"; import {isInInactivityLeak} from "../util/index.js"; /** - * Mutates `inactivityScores` from pre-calculated validator statuses. + * Mutates `inactivityScores` from pre-calculated validator flags. * * PERF: Cost = iterate over an array of size $VALIDATOR_COUNT + 'proportional' to how many validtors are inactive or * have been inactive in the past, i.e. that require an update to their inactivityScore. Worst case = all validators @@ -24,7 +24,7 @@ export function processInactivityUpdates(state: CachedBeaconStateAltair, cache: const {config, inactivityScores} = state; const {INACTIVITY_SCORE_BIAS, INACTIVITY_SCORE_RECOVERY_RATE} = config; - const {statuses, eligibleValidatorIndices} = cache; + const {flags, eligibleValidatorIndices} = cache; const inActivityLeak = isInInactivityLeak(state); // this avoids importing FLAG_ELIGIBLE_ATTESTER inside the for loop, check the compiled code @@ -34,11 +34,11 @@ export function processInactivityUpdates(state: CachedBeaconStateAltair, cache: for (let j = 0; j < eligibleValidatorIndices.length; j++) { const i = eligibleValidatorIndices[j]; - const status = statuses[i]; + const flag = flags[i]; let inactivityScore = inactivityScoresArr[i]; const prevInactivityScore = inactivityScore; - if (hasMarkers(status.flags, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { + if (hasMarkers(flag, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) { inactivityScore -= Math.min(1, inactivityScore); } else { inactivityScore += INACTIVITY_SCORE_BIAS; diff --git a/packages/state-transition/src/epoch/processPendingAttestations.ts b/packages/state-transition/src/epoch/processPendingAttestations.ts index 8f68e9735036..46644bd2d49f 100644 --- a/packages/state-transition/src/epoch/processPendingAttestations.ts +++ b/packages/state-transition/src/epoch/processPendingAttestations.ts @@ -4,7 +4,7 @@ import {CachedBeaconStatePhase0} from "../types.js"; import {computeStartSlotAtEpoch, getBlockRootAtSlot, AttesterStatus} from "../util/index.js"; /** - * Mutates `statuses` from all pending attestations. + * Mutates `statuses` and `flags` from all pending attestations. * * PERF: Cost 'proportional' to attestation count + how many bits per attestation + how many flags the attestation triggers * @@ -17,6 +17,7 @@ import {computeStartSlotAtEpoch, getBlockRootAtSlot, AttesterStatus} from "../ut export function processPendingAttestations( state: CachedBeaconStatePhase0, statuses: AttesterStatus[], + flags: Uint8Array, attestations: phase0.PendingAttestation[], epoch: Epoch, sourceFlag: number, @@ -62,12 +63,11 @@ export function processPendingAttestations( } for (const p of participants) { - const status = statuses[p]; - status.flags |= sourceFlag; + flags[p] |= sourceFlag; if (attVotedTargetRoot) { - status.flags |= targetFlag; + flags[p] |= targetFlag; if (attVotedHeadRoot) { - status.flags |= headFlag; + flags[p] |= headFlag; } } } diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index 62062bbfc539..567d8c51a63a 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -21,7 +21,12 @@ export type BeaconStateTransitionMetrics = { postStateBalancesNodesPopulatedHit: Gauge; postStateValidatorsNodesPopulatedMiss: Gauge; postStateValidatorsNodesPopulatedHit: Gauge; - registerValidatorStatuses: (currentEpoch: Epoch, statuses: AttesterStatus[], balances?: number[]) => void; + registerValidatorStatuses: ( + currentEpoch: Epoch, + statuses: AttesterStatus[], + flags: Uint8Array, + balances?: number[] + ) => void; }; export function onStateCloneMetrics( diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 7602f4d9acc2..dd0b783145e4 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -195,8 +195,8 @@ function processSlotsWithTransientCache( processEpoch(fork, postState, epochTransitionCache, metrics); - const {currentEpoch, statuses, balances} = epochTransitionCache; - metrics?.registerValidatorStatuses(currentEpoch, statuses, balances); + const {currentEpoch, statuses, flags, balances} = epochTransitionCache; + metrics?.registerValidatorStatuses(currentEpoch, statuses, flags, balances); postState.slot++; diff --git a/packages/state-transition/src/util/attesterStatus.ts b/packages/state-transition/src/util/attesterStatus.ts index 4f746615cbec..e492ed41c9d1 100644 --- a/packages/state-transition/src/util/attesterStatus.ts +++ b/packages/state-transition/src/util/attesterStatus.ts @@ -25,7 +25,6 @@ const TIMELY_HEAD = 1 << TIMELY_HEAD_FLAG_INDEX; * precomputed participation. */ export type AttesterStatus = { - flags: number; proposerIndex: number; // -1 when not included by any proposer inclusionDelay: number; active: boolean; @@ -33,7 +32,6 @@ export type AttesterStatus = { export function createAttesterStatus(): AttesterStatus { return { - flags: 0, proposerIndex: -1, inclusionDelay: 0, active: false, diff --git a/packages/state-transition/test/perf/analyzeEpochs.ts b/packages/state-transition/test/perf/analyzeEpochs.ts index c2f09fcc5521..6f61bc81abbc 100644 --- a/packages/state-transition/test/perf/analyzeEpochs.ts +++ b/packages/state-transition/test/perf/analyzeEpochs.ts @@ -114,8 +114,8 @@ async function analyzeEpochs(network: NetworkName, fromEpoch?: number): Promise< const attesterFlagsCount = {...attesterFlagsCountZero}; const keys = Object.keys(attesterFlagsCountZero) as (keyof typeof attesterFlagsCountZero)[]; - for (const status of cache.statuses) { - const flags = parseAttesterFlags(status.flags); + for (const flag of cache.flags) { + const flags = parseAttesterFlags(flag); for (const key of keys) { if (flags[key]) attesterFlagsCount[key]++; } diff --git a/packages/state-transition/test/perf/epoch/utilPhase0.ts b/packages/state-transition/test/perf/epoch/utilPhase0.ts index f73399ee108f..8f4e10fefd15 100644 --- a/packages/state-transition/test/perf/epoch/utilPhase0.ts +++ b/packages/state-transition/test/perf/epoch/utilPhase0.ts @@ -19,16 +19,17 @@ export function generateBalanceDeltasEpochTransitionCache( ): EpochTransitionCache { const vc = state.validators.length; - const statuses = generateStatuses(state.validators.length, flagFactors); + const {statuses, flags} = generateStatuses(state.validators.length, flagFactors); const eligibleValidatorIndices: number[] = []; - for (let i = 0; i < statuses.length; i++) { - if (hasMarkers(statuses[i].flags, FLAG_ELIGIBLE_ATTESTER)) { + for (let i = 0; i < flags.length; i++) { + if (hasMarkers(flags[i], FLAG_ELIGIBLE_ATTESTER)) { eligibleValidatorIndices.push(i); } } const cache: Partial = { statuses, + flags, eligibleValidatorIndices, totalActiveStakeByIncrement: vc, baseRewardPerIncrement: 726, @@ -45,19 +46,20 @@ export function generateBalanceDeltasEpochTransitionCache( export type FlagFactors = Record | number; -function generateStatuses(vc: number, flagFactors: FlagFactors): AttesterStatus[] { +function generateStatuses(vc: number, flagFactors: FlagFactors): {statuses: AttesterStatus[]; flags: Uint8Array} { const totalProposers = 32; const statuses = new Array(vc); + const flags = new Uint8Array(vc); for (let i = 0; i < vc; i++) { // Set to number to set all validators to the same value if (typeof flagFactors === "number") { statuses[i] = { - flags: flagFactors, proposerIndex: i % totalProposers, inclusionDelay: 1 + (i % 4), active: true, }; + flags[i] = flagFactors; } else { // Use a factor to set some validators to this flag const flagsObj: AttesterFlags = { @@ -71,13 +73,13 @@ function generateStatuses(vc: number, flagFactors: FlagFactors): AttesterStatus[ eligibleAttester: i < vc * flagFactors.eligibleAttester, // 7 }; statuses[i] = { - flags: toAttesterFlags(flagsObj), proposerIndex: i % totalProposers, inclusionDelay: 1 + (i % 4), active: true, }; + flags[i] = toAttesterFlags(flagsObj); } } - return statuses; + return {statuses, flags}; } From 8ec28133eb82381150a150ed3d5204a26d8812aa Mon Sep 17 00:00:00 2001 From: Cayman Date: Thu, 11 Jul 2024 09:34:21 -0400 Subject: [PATCH 02/11] chore: refactor active epoch indices --- .../src/metrics/validatorMonitor.ts | 11 +++---- .../src/cache/epochTransitionCache.ts | 29 ++++++++++++++----- packages/state-transition/src/metrics.ts | 1 + .../state-transition/src/stateTransition.ts | 4 +-- .../src/util/attesterStatus.ts | 2 -- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/beacon-node/src/metrics/validatorMonitor.ts b/packages/beacon-node/src/metrics/validatorMonitor.ts index df6680627f33..8699f97a7a75 100644 --- a/packages/beacon-node/src/metrics/validatorMonitor.ts +++ b/packages/beacon-node/src/metrics/validatorMonitor.ts @@ -43,6 +43,7 @@ export type ValidatorMonitor = { currentEpoch: Epoch, statuses: AttesterStatus[], flags: Uint8Array, + isActiveCurrEpoch: boolean[], balances?: number[] ): void; registerBeaconBlock(src: OpSource, seenTimestampSec: Seconds, block: BeaconBlock): void; @@ -120,12 +121,12 @@ type ValidatorStatus = { inclusionDistance: number; }; -function statusToSummary(status: AttesterStatus, flag: number): ValidatorStatus { +function statusToSummary(status: AttesterStatus, flag: number, isActiveInCurrentEpoch: boolean): ValidatorStatus { const flags = parseAttesterFlags(flag); return { isSlashed: flags.unslashed, - isActiveInCurrentEpoch: status.active, - isActiveInPreviousEpoch: status.active, + isActiveInCurrentEpoch, + isActiveInPreviousEpoch: isActiveInCurrentEpoch, // TODO: Implement currentEpochEffectiveBalance: 0, @@ -292,7 +293,7 @@ export function createValidatorMonitor( } }, - registerValidatorStatuses(currentEpoch, statuses, flags, balances) { + registerValidatorStatuses(currentEpoch, statuses, flags, isActiveCurrEpoch, balances) { // Prevent registering status for the same epoch twice. processEpoch() may be ran more than once for the same epoch. if (currentEpoch <= lastRegisteredStatusEpoch) { return; @@ -311,7 +312,7 @@ export function createValidatorMonitor( continue; } - const summary = statusToSummary(status, flags[index]); + const summary = statusToSummary(status, flags[index], isActiveCurrEpoch[index]); if (summary.isPrevSourceAttester) { metrics.validatorMonitor.prevEpochOnChainSourceAttesterHit.inc(); diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 491f75f307f0..a0b83232b00d 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -163,6 +163,12 @@ export interface EpochTransitionCache { */ nextEpochTotalActiveBalanceByIncrement: number; + /** + * Track by validator index if it's active in the current epoch. + * Used in metrics + */ + isActiveCurrEpoch: boolean[]; + /** * Track by validator index if it's active in the next epoch. * Used in `processEffectiveBalanceUpdates` to save one loop over validators after epoch process. @@ -190,8 +196,6 @@ export function beforeProcessEpoch( const indicesEligibleForActivation: ValidatorIndex[] = []; const indicesToEject: ValidatorIndex[] = []; const nextEpochShufflingActiveValidatorIndices: ValidatorIndex[] = []; - const isActivePrevEpoch: boolean[] = []; - const isActiveNextEpoch: boolean[] = []; const statuses: AttesterStatus[] = []; let totalActiveStakeByIncrement = 0; @@ -202,6 +206,10 @@ export function beforeProcessEpoch( const validators = state.validators.getAllReadonlyValues(); const validatorCount = validators.length; + // pre-fill with true (most validators are active) + const isActivePrevEpoch = new Array(validatorCount).fill(true); + const isActiveCurrEpoch = new Array(validatorCount).fill(true); + const isActiveNextEpoch = new Array(validatorCount).fill(true); // Flags for each validator // We can use Uint8Array since we only need 8 bits per validator // See src/util/attesterStatus.ts @@ -230,7 +238,9 @@ export function beforeProcessEpoch( const isActiveNext = activationEpoch <= nextEpoch && nextEpoch < exitEpoch; const isActiveNext2 = activationEpoch <= nextEpoch2 && nextEpoch2 < exitEpoch; - isActivePrevEpoch.push(isActivePrev); + if (!isActivePrev) { + isActivePrevEpoch[i] = false; + } // Both active validators and slashed-but-not-yet-withdrawn validators are eligible to receive penalties. // This is done to prevent self-slashing from being a way to escape inactivity leaks. @@ -241,8 +251,9 @@ export function beforeProcessEpoch( } if (isActiveCurr) { - status.active = true; totalActiveStakeByIncrement += effectiveBalancesByIncrements[i]; + } else { + isActiveCurrEpoch[i] = false; } // To optimize process_registry_updates(): @@ -285,7 +296,7 @@ export function beforeProcessEpoch( // // Use `else` since indicesEligibleForActivationQueue + indicesEligibleForActivation + indicesToEject are mutually exclusive else if ( - status.active && + isActiveCurr && validator.exitEpoch === FAR_FUTURE_EPOCH && validator.effectiveBalance <= config.EJECTION_BALANCE ) { @@ -294,7 +305,9 @@ export function beforeProcessEpoch( statuses.push(status); - isActiveNextEpoch.push(isActiveNext); + if (!isActiveNext) { + isActiveNextEpoch[i] = false; + } if (isActiveNext2) { nextEpochShufflingActiveValidatorIndices.push(i); @@ -349,9 +362,8 @@ export function beforeProcessEpoch( const currentEpochParticipation = (state as CachedBeaconStateAltair).currentEpochParticipation.getAll(); for (let i = 0; i < currentEpochParticipation.length; i++) { - const status = statuses[i]; // this is required to pass random spec tests in altair - if (status.active) { + if (isActiveCurrEpoch[i]) { // FLAG_PREV are indexes [3,4,5], so shift by 3 flags[i] |= currentEpochParticipation[i] << 3; } @@ -429,6 +441,7 @@ export function beforeProcessEpoch( nextEpochShufflingActiveValidatorIndices, // to be updated in processEffectiveBalanceUpdates nextEpochTotalActiveBalanceByIncrement: 0, + isActiveCurrEpoch, isActiveNextEpoch, statuses, flags, diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index 567d8c51a63a..d302b4c02ce2 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -25,6 +25,7 @@ export type BeaconStateTransitionMetrics = { currentEpoch: Epoch, statuses: AttesterStatus[], flags: Uint8Array, + isActiveCurrEpoch: boolean[], balances?: number[] ) => void; }; diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index dd0b783145e4..c314183fb0f9 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -195,8 +195,8 @@ function processSlotsWithTransientCache( processEpoch(fork, postState, epochTransitionCache, metrics); - const {currentEpoch, statuses, flags, balances} = epochTransitionCache; - metrics?.registerValidatorStatuses(currentEpoch, statuses, flags, balances); + const {currentEpoch, statuses, flags, isActiveCurrEpoch, balances} = epochTransitionCache; + metrics?.registerValidatorStatuses(currentEpoch, statuses, flags, isActiveCurrEpoch, balances); postState.slot++; diff --git a/packages/state-transition/src/util/attesterStatus.ts b/packages/state-transition/src/util/attesterStatus.ts index e492ed41c9d1..1a580d4ee905 100644 --- a/packages/state-transition/src/util/attesterStatus.ts +++ b/packages/state-transition/src/util/attesterStatus.ts @@ -27,14 +27,12 @@ const TIMELY_HEAD = 1 << TIMELY_HEAD_FLAG_INDEX; export type AttesterStatus = { proposerIndex: number; // -1 when not included by any proposer inclusionDelay: number; - active: boolean; }; export function createAttesterStatus(): AttesterStatus { return { proposerIndex: -1, inclusionDelay: 0, - active: false, }; } From 6124786fe8449b580669d91e5a4aa745e0e3c96b Mon Sep 17 00:00:00 2001 From: Cayman Date: Thu, 11 Jul 2024 10:53:28 -0400 Subject: [PATCH 03/11] chore: use number[] for flags --- .../src/metrics/validatorMonitor.ts | 2 +- .../src/cache/epochTransitionCache.ts | 5 +-- .../src/epoch/processPendingAttestations.ts | 2 +- packages/state-transition/src/metrics.ts | 2 +- .../test/perf/epoch/array.test.ts | 41 +++++++++++++++++++ .../test/perf/epoch/utilPhase0.ts | 6 +-- 6 files changed, 48 insertions(+), 10 deletions(-) create mode 100644 packages/state-transition/test/perf/epoch/array.test.ts diff --git a/packages/beacon-node/src/metrics/validatorMonitor.ts b/packages/beacon-node/src/metrics/validatorMonitor.ts index 8699f97a7a75..244d3dafd5de 100644 --- a/packages/beacon-node/src/metrics/validatorMonitor.ts +++ b/packages/beacon-node/src/metrics/validatorMonitor.ts @@ -42,7 +42,7 @@ export type ValidatorMonitor = { registerValidatorStatuses( currentEpoch: Epoch, statuses: AttesterStatus[], - flags: Uint8Array, + flags: number[], isActiveCurrEpoch: boolean[], balances?: number[] ): void; diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index a0b83232b00d..c16181a1b322 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -130,7 +130,7 @@ export interface EpochTransitionCache { */ statuses: AttesterStatus[]; - flags: Uint8Array; + flags: number[]; /** * balances array will be populated by processRewardsAndPenalties() and consumed by processEffectiveBalanceUpdates(). @@ -211,9 +211,8 @@ export function beforeProcessEpoch( const isActiveCurrEpoch = new Array(validatorCount).fill(true); const isActiveNextEpoch = new Array(validatorCount).fill(true); // Flags for each validator - // We can use Uint8Array since we only need 8 bits per validator // See src/util/attesterStatus.ts - const flags = new Uint8Array(validatorCount); + const flags = new Array(validatorCount).fill(0); // Clone before being mutated in processEffectiveBalanceUpdates epochCtx.beforeEpochTransition(); diff --git a/packages/state-transition/src/epoch/processPendingAttestations.ts b/packages/state-transition/src/epoch/processPendingAttestations.ts index 46644bd2d49f..c70c88be58ad 100644 --- a/packages/state-transition/src/epoch/processPendingAttestations.ts +++ b/packages/state-transition/src/epoch/processPendingAttestations.ts @@ -17,7 +17,7 @@ import {computeStartSlotAtEpoch, getBlockRootAtSlot, AttesterStatus} from "../ut export function processPendingAttestations( state: CachedBeaconStatePhase0, statuses: AttesterStatus[], - flags: Uint8Array, + flags: number[], attestations: phase0.PendingAttestation[], epoch: Epoch, sourceFlag: number, diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index d302b4c02ce2..0edca6338e7c 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -24,7 +24,7 @@ export type BeaconStateTransitionMetrics = { registerValidatorStatuses: ( currentEpoch: Epoch, statuses: AttesterStatus[], - flags: Uint8Array, + flags: number[], isActiveCurrEpoch: boolean[], balances?: number[] ) => void; diff --git a/packages/state-transition/test/perf/epoch/array.test.ts b/packages/state-transition/test/perf/epoch/array.test.ts new file mode 100644 index 000000000000..b83b5e0b09fe --- /dev/null +++ b/packages/state-transition/test/perf/epoch/array.test.ts @@ -0,0 +1,41 @@ +import {itBench} from "@dapplion/benchmark"; + +describe("array", () => { + const N = 1_000_000; + itBench({ + id: "Array.fill", + fn: () => { + const arr = new Array(N).fill(0); + for (let i = 0; i < N; i++) { + void 0; + } + }, + }); + itBench({ + id: "Array push", + fn: () => { + const arr: boolean[] = []; + for (let i = 0; i < N; i++) { + arr.push(true); + } + }, + }); + itBench({ + id: "Array.get", + beforeEach: () => { + return new Array(N).fill(8); + }, + fn: (arr) => { + arr[N - 1]; + }, + }); + itBench({ + id: "Uint8Array.get", + beforeEach: () => { + return new Uint8Array(N); + }, + fn: (arr) => { + arr[N - 1]; + }, + }); +}); diff --git a/packages/state-transition/test/perf/epoch/utilPhase0.ts b/packages/state-transition/test/perf/epoch/utilPhase0.ts index 8f4e10fefd15..a76c759e36e4 100644 --- a/packages/state-transition/test/perf/epoch/utilPhase0.ts +++ b/packages/state-transition/test/perf/epoch/utilPhase0.ts @@ -46,10 +46,10 @@ export function generateBalanceDeltasEpochTransitionCache( export type FlagFactors = Record | number; -function generateStatuses(vc: number, flagFactors: FlagFactors): {statuses: AttesterStatus[]; flags: Uint8Array} { +function generateStatuses(vc: number, flagFactors: FlagFactors): {statuses: AttesterStatus[]; flags: number[]} { const totalProposers = 32; const statuses = new Array(vc); - const flags = new Uint8Array(vc); + const flags = new Array(vc).fill(0); for (let i = 0; i < vc; i++) { // Set to number to set all validators to the same value @@ -57,7 +57,6 @@ function generateStatuses(vc: number, flagFactors: FlagFactors): {statuses: Atte statuses[i] = { proposerIndex: i % totalProposers, inclusionDelay: 1 + (i % 4), - active: true, }; flags[i] = flagFactors; } else { @@ -75,7 +74,6 @@ function generateStatuses(vc: number, flagFactors: FlagFactors): {statuses: Atte statuses[i] = { proposerIndex: i % totalProposers, inclusionDelay: 1 + (i % 4), - active: true, }; flags[i] = toAttesterFlags(flagsObj); } From 703e98352ab03d88f3334367838436bfba5cf0b8 Mon Sep 17 00:00:00 2001 From: Cayman Date: Thu, 11 Jul 2024 11:24:17 -0400 Subject: [PATCH 04/11] chore: fully decompose AttesterStatus --- .../src/metrics/validatorMonitor.ts | 16 +++------ .../src/cache/epochTransitionCache.ts | 31 +++++++++------- .../src/epoch/getAttestationDeltas.ts | 9 +++-- .../src/epoch/processPendingAttestations.ts | 14 ++++---- packages/state-transition/src/metrics.ts | 3 +- .../state-transition/src/stateTransition.ts | 4 +-- .../src/util/attesterStatus.ts | 17 --------- .../test/perf/epoch/utilPhase0.ts | 35 ++++++++----------- 8 files changed, 53 insertions(+), 76 deletions(-) diff --git a/packages/beacon-node/src/metrics/validatorMonitor.ts b/packages/beacon-node/src/metrics/validatorMonitor.ts index 244d3dafd5de..4527630703ca 100644 --- a/packages/beacon-node/src/metrics/validatorMonitor.ts +++ b/packages/beacon-node/src/metrics/validatorMonitor.ts @@ -1,6 +1,5 @@ import { computeEpochAtSlot, - AttesterStatus, parseAttesterFlags, CachedBeaconStateAllForks, CachedBeaconStateAltair, @@ -41,7 +40,7 @@ export type ValidatorMonitor = { registerLocalValidatorInSyncCommittee(index: number, untilEpoch: Epoch): void; registerValidatorStatuses( currentEpoch: Epoch, - statuses: AttesterStatus[], + inclusionDelays: number[], flags: number[], isActiveCurrEpoch: boolean[], balances?: number[] @@ -121,7 +120,7 @@ type ValidatorStatus = { inclusionDistance: number; }; -function statusToSummary(status: AttesterStatus, flag: number, isActiveInCurrentEpoch: boolean): ValidatorStatus { +function statusToSummary(inclusionDelay: number, flag: number, isActiveInCurrentEpoch: boolean): ValidatorStatus { const flags = parseAttesterFlags(flag); return { isSlashed: flags.unslashed, @@ -136,7 +135,7 @@ function statusToSummary(status: AttesterStatus, flag: number, isActiveInCurrent isCurrSourceAttester: flags.currSourceAttester, isCurrTargetAttester: flags.currTargetAttester, isCurrHeadAttester: flags.currHeadAttester, - inclusionDistance: status.inclusionDelay, + inclusionDistance: inclusionDelay, }; } @@ -293,7 +292,7 @@ export function createValidatorMonitor( } }, - registerValidatorStatuses(currentEpoch, statuses, flags, isActiveCurrEpoch, balances) { + registerValidatorStatuses(currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, balances) { // Prevent registering status for the same epoch twice. processEpoch() may be ran more than once for the same epoch. if (currentEpoch <= lastRegisteredStatusEpoch) { return; @@ -307,12 +306,7 @@ export function createValidatorMonitor( // - One to account for it being the previous epoch. // - One to account for the state advancing an epoch whilst generating the validator // statuses. - const status = statuses[index]; - if (status === undefined) { - continue; - } - - const summary = statusToSummary(status, flags[index], isActiveCurrEpoch[index]); + const summary = statusToSummary(inclusionDelays[index], flags[index], isActiveCurrEpoch[index]); if (summary.isPrevSourceAttester) { metrics.validatorMonitor.prevEpochOnChainSourceAttesterHit.inc(); diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index c16181a1b322..dee22baf842c 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -3,8 +3,6 @@ import {intDiv} from "@lodestar/utils"; import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; import { - AttesterStatus, - createAttesterStatus, hasMarkers, FLAG_UNSLASHED, FLAG_ELIGIBLE_ATTESTER, @@ -128,7 +126,10 @@ export interface EpochTransitionCache { * - prev attester flag set * With a status flag to check this conditions at once we just have to mask with an OR of the conditions. */ - statuses: AttesterStatus[]; + + proposerIndices: number[]; + + inclusionDelays: number[]; flags: number[]; @@ -196,7 +197,6 @@ export function beforeProcessEpoch( const indicesEligibleForActivation: ValidatorIndex[] = []; const indicesToEject: ValidatorIndex[] = []; const nextEpochShufflingActiveValidatorIndices: ValidatorIndex[] = []; - const statuses: AttesterStatus[] = []; let totalActiveStakeByIncrement = 0; @@ -210,8 +210,15 @@ export function beforeProcessEpoch( const isActivePrevEpoch = new Array(validatorCount).fill(true); const isActiveCurrEpoch = new Array(validatorCount).fill(true); const isActiveNextEpoch = new Array(validatorCount).fill(true); - // Flags for each validator - // See src/util/attesterStatus.ts + + // During the epoch transition, additional data is precomputed to avoid traversing any state a second + // time. Attestations are a big part of this, and each validator has a "status" to represent its + // precomputed participation. + // - proposerIndex: number; // -1 when not included by any proposer + // - inclusionDelay: number; + // - flags: number; // bitfield of AttesterFlags + const proposerIndices = new Array(validatorCount).fill(-1); + const inclusionDelays = new Array(validatorCount).fill(0); const flags = new Array(validatorCount).fill(0); // Clone before being mutated in processEffectiveBalanceUpdates @@ -221,7 +228,6 @@ export function beforeProcessEpoch( for (let i = 0; i < validatorCount; i++) { const validator = validators[i]; - const status = createAttesterStatus(); if (validator.slashed) { if (slashingsEpoch === validator.withdrawableEpoch) { @@ -302,8 +308,6 @@ export function beforeProcessEpoch( indicesToEject.push(i); } - statuses.push(status); - if (!isActiveNext) { isActiveNextEpoch[i] = false; } @@ -331,7 +335,8 @@ export function beforeProcessEpoch( if (forkSeq === ForkSeq.phase0) { processPendingAttestations( state as CachedBeaconStatePhase0, - statuses, + proposerIndices, + inclusionDelays, flags, (state as CachedBeaconStatePhase0).previousEpochAttestations.getAllReadonly(), prevEpoch, @@ -341,7 +346,8 @@ export function beforeProcessEpoch( ); processPendingAttestations( state as CachedBeaconStatePhase0, - statuses, + proposerIndices, + inclusionDelays, flags, (state as CachedBeaconStatePhase0).currentEpochAttestations.getAllReadonly(), currentEpoch, @@ -442,7 +448,8 @@ export function beforeProcessEpoch( nextEpochTotalActiveBalanceByIncrement: 0, isActiveCurrEpoch, isActiveNextEpoch, - statuses, + proposerIndices, + inclusionDelays, flags, // Will be assigned in processRewardsAndPenalties() diff --git a/packages/state-transition/src/epoch/getAttestationDeltas.ts b/packages/state-transition/src/epoch/getAttestationDeltas.ts index 72f599ebc190..dd69738a55b5 100644 --- a/packages/state-transition/src/epoch/getAttestationDeltas.ts +++ b/packages/state-transition/src/epoch/getAttestationDeltas.ts @@ -52,7 +52,7 @@ export function getAttestationDeltas( state: CachedBeaconStatePhase0, cache: EpochTransitionCache ): [number[], number[]] { - const {flags, statuses} = cache; + const {flags, proposerIndices, inclusionDelays} = cache; const validatorCount = flags.length; const rewards = newZeroedArray(validatorCount); const penalties = newZeroedArray(validatorCount); @@ -79,9 +79,8 @@ export function getAttestationDeltas( // so there are limited values of them like 32, 31, 30 const rewardPnaltyItemCache = new Map(); const {effectiveBalanceIncrements} = state.epochCtx; - for (let i = 0; i < statuses.length; i++) { + for (let i = 0; i < flags.length; i++) { const flag = flags[i]; - const status = statuses[i]; const effectiveBalanceIncrement = effectiveBalanceIncrements[i]; const effectiveBalance = effectiveBalanceIncrement * EFFECTIVE_BALANCE_INCREMENT; @@ -123,8 +122,8 @@ export function getAttestationDeltas( // inclusion speed bonus if (hasMarkers(flag, FLAG_PREV_SOURCE_ATTESTER_OR_UNSLASHED)) { - rewards[status.proposerIndex] += proposerReward; - rewards[i] += Math.floor(maxAttesterReward / status.inclusionDelay); + rewards[proposerIndices[i]] += proposerReward; + rewards[i] += Math.floor(maxAttesterReward / inclusionDelays[i]); } if (hasMarkers(flag, FLAG_ELIGIBLE_ATTESTER)) { diff --git a/packages/state-transition/src/epoch/processPendingAttestations.ts b/packages/state-transition/src/epoch/processPendingAttestations.ts index c70c88be58ad..a6043be77524 100644 --- a/packages/state-transition/src/epoch/processPendingAttestations.ts +++ b/packages/state-transition/src/epoch/processPendingAttestations.ts @@ -1,10 +1,10 @@ import {byteArrayEquals} from "@chainsafe/ssz"; import {Epoch, phase0} from "@lodestar/types"; import {CachedBeaconStatePhase0} from "../types.js"; -import {computeStartSlotAtEpoch, getBlockRootAtSlot, AttesterStatus} from "../util/index.js"; +import {computeStartSlotAtEpoch, getBlockRootAtSlot} from "../util/index.js"; /** - * Mutates `statuses` and `flags` from all pending attestations. + * Mutates `proposerIndices`, `inclusionDelays` and `flags` from all pending attestations. * * PERF: Cost 'proportional' to attestation count + how many bits per attestation + how many flags the attestation triggers * @@ -16,7 +16,8 @@ import {computeStartSlotAtEpoch, getBlockRootAtSlot, AttesterStatus} from "../ut */ export function processPendingAttestations( state: CachedBeaconStatePhase0, - statuses: AttesterStatus[], + proposerIndices: number[], + inclusionDelays: number[], flags: number[], attestations: phase0.PendingAttestation[], epoch: Epoch, @@ -54,10 +55,9 @@ export function processPendingAttestations( if (epoch === prevEpoch) { for (const p of participants) { - const status = statuses[p]; - if (status.proposerIndex === -1 || status.inclusionDelay > inclusionDelay) { - status.proposerIndex = proposerIndex; - status.inclusionDelay = inclusionDelay; + if (proposerIndices[p] === -1 || inclusionDelays[p] > inclusionDelay) { + proposerIndices[p] = proposerIndex; + inclusionDelays[p] = inclusionDelay; } } } diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index 0edca6338e7c..ab27ecf29976 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -2,7 +2,6 @@ import {Epoch} from "@lodestar/types"; import {Gauge, Histogram} from "@lodestar/utils"; import {CachedBeaconStateAllForks} from "./types.js"; import {StateCloneSource, StateHashTreeRootSource} from "./stateTransition.js"; -import {AttesterStatus} from "./util/attesterStatus.js"; import {EpochTransitionStep} from "./epoch/index.js"; export type BeaconStateTransitionMetrics = { @@ -23,7 +22,7 @@ export type BeaconStateTransitionMetrics = { postStateValidatorsNodesPopulatedHit: Gauge; registerValidatorStatuses: ( currentEpoch: Epoch, - statuses: AttesterStatus[], + inclusionDelays: number[], flags: number[], isActiveCurrEpoch: boolean[], balances?: number[] diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index c314183fb0f9..8fec8580681b 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -195,8 +195,8 @@ function processSlotsWithTransientCache( processEpoch(fork, postState, epochTransitionCache, metrics); - const {currentEpoch, statuses, flags, isActiveCurrEpoch, balances} = epochTransitionCache; - metrics?.registerValidatorStatuses(currentEpoch, statuses, flags, isActiveCurrEpoch, balances); + const {currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, balances} = epochTransitionCache; + metrics?.registerValidatorStatuses(currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, balances); postState.slot++; diff --git a/packages/state-transition/src/util/attesterStatus.ts b/packages/state-transition/src/util/attesterStatus.ts index 1a580d4ee905..5d60653f15a1 100644 --- a/packages/state-transition/src/util/attesterStatus.ts +++ b/packages/state-transition/src/util/attesterStatus.ts @@ -19,23 +19,6 @@ const TIMELY_SOURCE = 1 << TIMELY_SOURCE_FLAG_INDEX; const TIMELY_TARGET = 1 << TIMELY_TARGET_FLAG_INDEX; const TIMELY_HEAD = 1 << TIMELY_HEAD_FLAG_INDEX; -/** - * During the epoch transition, additional data is precomputed to avoid traversing any state a second - * time. Attestations are a big part of this, and each validator has a "status" to represent its - * precomputed participation. - */ -export type AttesterStatus = { - proposerIndex: number; // -1 when not included by any proposer - inclusionDelay: number; -}; - -export function createAttesterStatus(): AttesterStatus { - return { - proposerIndex: -1, - inclusionDelay: 0, - }; -} - export function hasMarkers(flags: number, markers: number): boolean { return (flags & markers) === markers; } diff --git a/packages/state-transition/test/perf/epoch/utilPhase0.ts b/packages/state-transition/test/perf/epoch/utilPhase0.ts index a76c759e36e4..026506510979 100644 --- a/packages/state-transition/test/perf/epoch/utilPhase0.ts +++ b/packages/state-transition/test/perf/epoch/utilPhase0.ts @@ -1,10 +1,4 @@ -import { - AttesterFlags, - FLAG_ELIGIBLE_ATTESTER, - hasMarkers, - AttesterStatus, - toAttesterFlags, -} from "../../../src/index.js"; +import {AttesterFlags, FLAG_ELIGIBLE_ATTESTER, hasMarkers, toAttesterFlags} from "../../../src/index.js"; import {CachedBeaconStatePhase0, CachedBeaconStateAltair, EpochTransitionCache} from "../../../src/types.js"; /** @@ -19,7 +13,7 @@ export function generateBalanceDeltasEpochTransitionCache( ): EpochTransitionCache { const vc = state.validators.length; - const {statuses, flags} = generateStatuses(state.validators.length, flagFactors); + const {proposerIndices, inclusionDelays, flags} = generateStatuses(state.validators.length, flagFactors); const eligibleValidatorIndices: number[] = []; for (let i = 0; i < flags.length; i++) { if (hasMarkers(flags[i], FLAG_ELIGIBLE_ATTESTER)) { @@ -28,7 +22,8 @@ export function generateBalanceDeltasEpochTransitionCache( } const cache: Partial = { - statuses, + proposerIndices, + inclusionDelays, flags, eligibleValidatorIndices, totalActiveStakeByIncrement: vc, @@ -46,18 +41,20 @@ export function generateBalanceDeltasEpochTransitionCache( export type FlagFactors = Record | number; -function generateStatuses(vc: number, flagFactors: FlagFactors): {statuses: AttesterStatus[]; flags: number[]} { +function generateStatuses( + vc: number, + flagFactors: FlagFactors +): {proposerIndices: number[]; inclusionDelays: number[]; flags: number[]} { const totalProposers = 32; - const statuses = new Array(vc); + const proposerIndices = new Array(vc); + const inclusionDelays = new Array(vc); const flags = new Array(vc).fill(0); for (let i = 0; i < vc; i++) { // Set to number to set all validators to the same value if (typeof flagFactors === "number") { - statuses[i] = { - proposerIndex: i % totalProposers, - inclusionDelay: 1 + (i % 4), - }; + proposerIndices[i] = i % totalProposers; + inclusionDelays[i] = 1 + (i % 4); flags[i] = flagFactors; } else { // Use a factor to set some validators to this flag @@ -71,13 +68,11 @@ function generateStatuses(vc: number, flagFactors: FlagFactors): {statuses: Atte unslashed: i < vc * flagFactors.unslashed, // 6 eligibleAttester: i < vc * flagFactors.eligibleAttester, // 7 }; - statuses[i] = { - proposerIndex: i % totalProposers, - inclusionDelay: 1 + (i % 4), - }; + proposerIndices[i] = i % totalProposers; + inclusionDelays[i] = 1 + (i % 4); flags[i] = toAttesterFlags(flagsObj); } } - return {statuses, flags}; + return {proposerIndices, inclusionDelays, flags}; } From 86deaef52141d69620228b746ee9f0c81662e6d7 Mon Sep 17 00:00:00 2001 From: Cayman Date: Thu, 11 Jul 2024 13:17:46 -0400 Subject: [PATCH 05/11] chore: fix lint --- packages/state-transition/src/cache/epochTransitionCache.ts | 6 +++--- packages/state-transition/test/perf/epoch/array.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index dee22baf842c..28b6933803a1 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -217,9 +217,9 @@ export function beforeProcessEpoch( // - proposerIndex: number; // -1 when not included by any proposer // - inclusionDelay: number; // - flags: number; // bitfield of AttesterFlags - const proposerIndices = new Array(validatorCount).fill(-1); - const inclusionDelays = new Array(validatorCount).fill(0); - const flags = new Array(validatorCount).fill(0); + const proposerIndices = new Array(validatorCount).fill(-1); + const inclusionDelays = new Array(validatorCount).fill(0); + const flags = new Array(validatorCount).fill(0); // Clone before being mutated in processEffectiveBalanceUpdates epochCtx.beforeEpochTransition(); diff --git a/packages/state-transition/test/perf/epoch/array.test.ts b/packages/state-transition/test/perf/epoch/array.test.ts index b83b5e0b09fe..b221bd77b292 100644 --- a/packages/state-transition/test/perf/epoch/array.test.ts +++ b/packages/state-transition/test/perf/epoch/array.test.ts @@ -5,7 +5,7 @@ describe("array", () => { itBench({ id: "Array.fill", fn: () => { - const arr = new Array(N).fill(0); + new Array(N).fill(0); for (let i = 0; i < N; i++) { void 0; } From da5942eae4e3f05843e8c5d46c55058e17405719 Mon Sep 17 00:00:00 2001 From: Cayman Date: Fri, 12 Jul 2024 09:51:15 -0400 Subject: [PATCH 06/11] chore: reuse epoch transition cache arrays --- .../src/cache/epochTransitionCache.ts | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 28b6933803a1..227d54ca6c9e 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -177,6 +177,21 @@ export interface EpochTransitionCache { isActiveNextEpoch: boolean[]; } +// reuse arrays to avoid memory reallocation and gc +// WARNING: this is not async safe +/** WARNING: reused, never gc'd */ +const isActivePrevEpoch = new Array(); +/** WARNING: reused, never gc'd */ +const isActiveCurrEpoch = new Array(); +/** WARNING: reused, never gc'd */ +const isActiveNextEpoch = new Array(); +/** WARNING: reused, never gc'd */ +const proposerIndices = new Array(); +/** WARNING: reused, never gc'd */ +const inclusionDelays = new Array(); +/** WARNING: reused, never gc'd */ +const flags = new Array(); + export function beforeProcessEpoch( state: CachedBeaconStateAllForks, opts?: EpochTransitionCacheOpts @@ -207,9 +222,12 @@ export function beforeProcessEpoch( const validatorCount = validators.length; // pre-fill with true (most validators are active) - const isActivePrevEpoch = new Array(validatorCount).fill(true); - const isActiveCurrEpoch = new Array(validatorCount).fill(true); - const isActiveNextEpoch = new Array(validatorCount).fill(true); + isActivePrevEpoch.length = validatorCount; + isActiveCurrEpoch.length = validatorCount; + isActiveNextEpoch.length = validatorCount; + isActivePrevEpoch.fill(true); + isActiveCurrEpoch.fill(true); + isActiveNextEpoch.fill(true); // During the epoch transition, additional data is precomputed to avoid traversing any state a second // time. Attestations are a big part of this, and each validator has a "status" to represent its @@ -217,9 +235,12 @@ export function beforeProcessEpoch( // - proposerIndex: number; // -1 when not included by any proposer // - inclusionDelay: number; // - flags: number; // bitfield of AttesterFlags - const proposerIndices = new Array(validatorCount).fill(-1); - const inclusionDelays = new Array(validatorCount).fill(0); - const flags = new Array(validatorCount).fill(0); + proposerIndices.length = validatorCount; + inclusionDelays.length = validatorCount; + flags.length = validatorCount; + proposerIndices.fill(-1); + inclusionDelays.fill(0); + flags.fill(0); // Clone before being mutated in processEffectiveBalanceUpdates epochCtx.beforeEpochTransition(); From bbc7f697f270c127c5ed72da6fd394bbdcce679a Mon Sep 17 00:00:00 2001 From: Cayman Date: Sun, 14 Jul 2024 11:44:40 -0400 Subject: [PATCH 07/11] chore: add isActivePrevEpoch to metrics --- .../src/metrics/validatorMonitor.ts | 19 +++++++++++++++---- .../src/cache/epochTransitionCache.ts | 7 +++++++ packages/state-transition/src/metrics.ts | 1 + .../state-transition/src/stateTransition.ts | 12 ++++++++++-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/beacon-node/src/metrics/validatorMonitor.ts b/packages/beacon-node/src/metrics/validatorMonitor.ts index 4527630703ca..e5c8eef83679 100644 --- a/packages/beacon-node/src/metrics/validatorMonitor.ts +++ b/packages/beacon-node/src/metrics/validatorMonitor.ts @@ -43,6 +43,7 @@ export type ValidatorMonitor = { inclusionDelays: number[], flags: number[], isActiveCurrEpoch: boolean[], + isActivePrevEpoch: boolean[], balances?: number[] ): void; registerBeaconBlock(src: OpSource, seenTimestampSec: Seconds, block: BeaconBlock): void; @@ -120,12 +121,17 @@ type ValidatorStatus = { inclusionDistance: number; }; -function statusToSummary(inclusionDelay: number, flag: number, isActiveInCurrentEpoch: boolean): ValidatorStatus { +function statusToSummary( + inclusionDelay: number, + flag: number, + isActiveInCurrentEpoch: boolean, + isActiveInPreviousEpoch: boolean +): ValidatorStatus { const flags = parseAttesterFlags(flag); return { isSlashed: flags.unslashed, isActiveInCurrentEpoch, - isActiveInPreviousEpoch: isActiveInCurrentEpoch, + isActiveInPreviousEpoch, // TODO: Implement currentEpochEffectiveBalance: 0, @@ -292,7 +298,7 @@ export function createValidatorMonitor( } }, - registerValidatorStatuses(currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, balances) { + registerValidatorStatuses(currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, isActiveInPrevEpoch, balances) { // Prevent registering status for the same epoch twice. processEpoch() may be ran more than once for the same epoch. if (currentEpoch <= lastRegisteredStatusEpoch) { return; @@ -306,7 +312,12 @@ export function createValidatorMonitor( // - One to account for it being the previous epoch. // - One to account for the state advancing an epoch whilst generating the validator // statuses. - const summary = statusToSummary(inclusionDelays[index], flags[index], isActiveCurrEpoch[index]); + const summary = statusToSummary( + inclusionDelays[index], + flags[index], + isActiveCurrEpoch[index], + isActiveInPrevEpoch[index] + ); if (summary.isPrevSourceAttester) { metrics.validatorMonitor.prevEpochOnChainSourceAttesterHit.inc(); diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 227d54ca6c9e..9bbcb4b93a1f 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -164,6 +164,12 @@ export interface EpochTransitionCache { */ nextEpochTotalActiveBalanceByIncrement: number; + /** + * Track by validator index if it's active in the prev epoch. + * Used in metrics + */ + isActivePrevEpoch: boolean[]; + /** * Track by validator index if it's active in the current epoch. * Used in metrics @@ -467,6 +473,7 @@ export function beforeProcessEpoch( nextEpochShufflingActiveValidatorIndices, // to be updated in processEffectiveBalanceUpdates nextEpochTotalActiveBalanceByIncrement: 0, + isActivePrevEpoch, isActiveCurrEpoch, isActiveNextEpoch, proposerIndices, diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index ab27ecf29976..12cec46d9a49 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -25,6 +25,7 @@ export type BeaconStateTransitionMetrics = { inclusionDelays: number[], flags: number[], isActiveCurrEpoch: boolean[], + isActivePrevEpoch: boolean[], balances?: number[] ) => void; }; diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 8fec8580681b..78bcaa140c62 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -195,8 +195,16 @@ function processSlotsWithTransientCache( processEpoch(fork, postState, epochTransitionCache, metrics); - const {currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, balances} = epochTransitionCache; - metrics?.registerValidatorStatuses(currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, balances); + const {currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, isActivePrevEpoch, balances} = + epochTransitionCache; + metrics?.registerValidatorStatuses( + currentEpoch, + inclusionDelays, + flags, + isActiveCurrEpoch, + isActivePrevEpoch, + balances + ); postState.slot++; From 1dbfb660edf02795c13e9849201f47465e16623a Mon Sep 17 00:00:00 2001 From: Cayman Date: Sun, 14 Jul 2024 12:03:32 -0400 Subject: [PATCH 08/11] chore: remove dead code --- .../src/util/attesterStatus.ts | 41 +++++++------------ 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/packages/state-transition/src/util/attesterStatus.ts b/packages/state-transition/src/util/attesterStatus.ts index 5d60653f15a1..4859983df86a 100644 --- a/packages/state-transition/src/util/attesterStatus.ts +++ b/packages/state-transition/src/util/attesterStatus.ts @@ -1,24 +1,27 @@ import {TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX} from "@lodestar/params"; -export const FLAG_PREV_SOURCE_ATTESTER = 1 << 0; -export const FLAG_PREV_TARGET_ATTESTER = 1 << 1; -export const FLAG_PREV_HEAD_ATTESTER = 1 << 2; -export const FLAG_CURR_SOURCE_ATTESTER = 1 << 3; -export const FLAG_CURR_TARGET_ATTESTER = 1 << 4; -export const FLAG_CURR_HEAD_ATTESTER = 1 << 5; +// We pack both previous and current epoch attester flags +// as well as slashed and eligibility flags into a single number +// to save space in our epoch transition cache. +// Note: the order of the flags is important for efficiently translating +// from the BeaconState flags to our flags. +// [prevSource, prevTarget, prevHead, currSource, currTarget, currHead, unslashed, eligible] +export const FLAG_PREV_SOURCE_ATTESTER = 1 << TIMELY_SOURCE_FLAG_INDEX; +export const FLAG_PREV_TARGET_ATTESTER = 1 << TIMELY_TARGET_FLAG_INDEX; +export const FLAG_PREV_HEAD_ATTESTER = 1 << TIMELY_HEAD_FLAG_INDEX; + +export const FLAG_CURR_SOURCE_ATTESTER = 1 << (3 + TIMELY_SOURCE_FLAG_INDEX); +export const FLAG_CURR_TARGET_ATTESTER = 1 << (3 + TIMELY_TARGET_FLAG_INDEX); +export const FLAG_CURR_HEAD_ATTESTER = 1 << (3 + TIMELY_HEAD_FLAG_INDEX); export const FLAG_UNSLASHED = 1 << 6; export const FLAG_ELIGIBLE_ATTESTER = 1 << 7; -// Precompute OR flags + +// Precompute OR flags used in epoch processing export const FLAG_PREV_SOURCE_ATTESTER_UNSLASHED = FLAG_PREV_SOURCE_ATTESTER | FLAG_UNSLASHED; export const FLAG_PREV_TARGET_ATTESTER_UNSLASHED = FLAG_PREV_TARGET_ATTESTER | FLAG_UNSLASHED; export const FLAG_PREV_HEAD_ATTESTER_UNSLASHED = FLAG_PREV_HEAD_ATTESTER | FLAG_UNSLASHED; -/** Same to https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.5/specs/altair/beacon-chain.md#has_flag */ -const TIMELY_SOURCE = 1 << TIMELY_SOURCE_FLAG_INDEX; -const TIMELY_TARGET = 1 << TIMELY_TARGET_FLAG_INDEX; -const TIMELY_HEAD = 1 << TIMELY_HEAD_FLAG_INDEX; - export function hasMarkers(flags: number, markers: number): boolean { return (flags & markers) === markers; } @@ -59,17 +62,3 @@ export function toAttesterFlags(flagsObj: AttesterFlags): number { if (flagsObj.eligibleAttester) flag |= FLAG_ELIGIBLE_ATTESTER; return flag; } - -export type ParticipationFlags = { - timelySource: boolean; - timelyTarget: boolean; - timelyHead: boolean; -}; - -export function parseParticipationFlags(flags: number): ParticipationFlags { - return { - timelySource: hasMarkers(flags, TIMELY_SOURCE), - timelyTarget: hasMarkers(flags, TIMELY_TARGET), - timelyHead: hasMarkers(flags, TIMELY_HEAD), - }; -} From ab9efd32d418c64bd74bcb9d7138139f1687dad6 Mon Sep 17 00:00:00 2001 From: Cayman Date: Sun, 14 Jul 2024 12:21:44 -0400 Subject: [PATCH 09/11] chore: refactor --- .../src/cache/epochTransitionCache.ts | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 9bbcb4b93a1f..dfe6bdd8e102 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -246,7 +246,12 @@ export function beforeProcessEpoch( flags.length = validatorCount; proposerIndices.fill(-1); inclusionDelays.fill(0); - flags.fill(0); + // flags.fill(0); + // flags will be zero'd out below + // In the first loop, set slashed+eligibility + // In the second loop, set participation flags + // TODO: optimize by combining the two loops + // likely will require splitting into phase0 and post-phase0 versions // Clone before being mutated in processEffectiveBalanceUpdates epochCtx.beforeEpochTransition(); @@ -255,13 +260,14 @@ export function beforeProcessEpoch( for (let i = 0; i < validatorCount; i++) { const validator = validators[i]; + let flag = 0; if (validator.slashed) { if (slashingsEpoch === validator.withdrawableEpoch) { indicesToSlash.push(i); } } else { - flags[i] |= FLAG_UNSLASHED; + flag |= FLAG_UNSLASHED; } const {activationEpoch, exitEpoch} = validator; @@ -279,9 +285,11 @@ export function beforeProcessEpoch( // TODO: Consider using an array of `eligibleValidatorIndices: number[]` if (isActivePrev || (validator.slashed && prevEpoch + 1 < validator.withdrawableEpoch)) { eligibleValidatorIndices.push(i); - flags[i] |= FLAG_ELIGIBLE_ATTESTER; + flag |= FLAG_ELIGIBLE_ATTESTER; } + flags[i] = flag; + if (isActiveCurr) { totalActiveStakeByIncrement += effectiveBalancesByIncrements[i]; } else { @@ -384,21 +392,15 @@ export function beforeProcessEpoch( ); } else { const previousEpochParticipation = (state as CachedBeaconStateAltair).previousEpochParticipation.getAll(); - for (let i = 0; i < previousEpochParticipation.length; i++) { - // this is required to pass random spec tests in altair - if (isActivePrevEpoch[i]) { - // FLAG_PREV are indexes [0,1,2] - flags[i] |= previousEpochParticipation[i]; - } - } - const currentEpochParticipation = (state as CachedBeaconStateAltair).currentEpochParticipation.getAll(); - for (let i = 0; i < currentEpochParticipation.length; i++) { - // this is required to pass random spec tests in altair - if (isActiveCurrEpoch[i]) { - // FLAG_PREV are indexes [3,4,5], so shift by 3 - flags[i] |= currentEpochParticipation[i] << 3; - } + for (let i = 0; i < validatorCount; i++) { + flags[i] |= + // checking active status first is required to pass random spec tests in altair + // in practice, inactive validators will have 0 participation + // FLAG_PREV are indexes [0,1,2] + (isActivePrevEpoch[i] ? previousEpochParticipation[i] : 0) | + // FLAG_CURR are indexes [3,4,5], so shift by 3 + (isActiveCurrEpoch[i] ? currentEpochParticipation[i] << 3 : 0); } } From fd0eefa2c23a0745def27f49fc763cc7c3c28c2b Mon Sep 17 00:00:00 2001 From: Cayman Date: Sun, 14 Jul 2024 12:29:37 -0400 Subject: [PATCH 10/11] chore: fix up perf tests --- .../test/perf/epoch/array.test.ts | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/state-transition/test/perf/epoch/array.test.ts b/packages/state-transition/test/perf/epoch/array.test.ts index b221bd77b292..eafd349e982f 100644 --- a/packages/state-transition/test/perf/epoch/array.test.ts +++ b/packages/state-transition/test/perf/epoch/array.test.ts @@ -1,9 +1,22 @@ import {itBench} from "@dapplion/benchmark"; +/* +July 14, 2024 +- AMD Ryzen Threadripper 1950X 16-Core Processor +- Linux 5.15.0-113-generic +- Node v20.12.2 + + array + ✔ Array.fill - length 1000000 148.1271 ops/s 6.750961 ms/op - 109 runs 1.24 s + ✔ Array push - length 1000000 35.63945 ops/s 28.05879 ms/op - 158 runs 4.97 s + ✔ Array.get 2.002555e+9 ops/s 0.4993620 ns/op - 66 runs 7.96 s + ✔ Uint8Array.get 2.002383e+9 ops/s 0.4994050 ns/op - 512 runs 0.903 s +*/ + describe("array", () => { const N = 1_000_000; itBench({ - id: "Array.fill", + id: `Array.fill - length ${N}`, fn: () => { new Array(N).fill(0); for (let i = 0; i < N; i++) { @@ -12,7 +25,7 @@ describe("array", () => { }, }); itBench({ - id: "Array push", + id: `Array push - length ${N}`, fn: () => { const arr: boolean[] = []; for (let i = 0; i < N; i++) { @@ -22,20 +35,26 @@ describe("array", () => { }); itBench({ id: "Array.get", + runsFactor: N, beforeEach: () => { return new Array(N).fill(8); }, fn: (arr) => { - arr[N - 1]; + for (let i = 0; i < N; i++) { + arr[N - 1]; + } }, }); itBench({ id: "Uint8Array.get", + runsFactor: N, beforeEach: () => { return new Uint8Array(N); }, fn: (arr) => { - arr[N - 1]; + for (let i = 0; i < N; i++) { + arr[N - 1]; + } }, }); }); From 21e372853e328618faef81b77432f21190a82f36 Mon Sep 17 00:00:00 2001 From: Cayman Date: Sun, 14 Jul 2024 13:08:26 -0400 Subject: [PATCH 11/11] chore: add functions back --- .../src/util/attesterStatus.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/state-transition/src/util/attesterStatus.ts b/packages/state-transition/src/util/attesterStatus.ts index 4859983df86a..effacef47e06 100644 --- a/packages/state-transition/src/util/attesterStatus.ts +++ b/packages/state-transition/src/util/attesterStatus.ts @@ -62,3 +62,22 @@ export function toAttesterFlags(flagsObj: AttesterFlags): number { if (flagsObj.eligibleAttester) flag |= FLAG_ELIGIBLE_ATTESTER; return flag; } + +/** Same to https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.5/specs/altair/beacon-chain.md#has_flag */ +const TIMELY_SOURCE = 1 << TIMELY_SOURCE_FLAG_INDEX; +const TIMELY_TARGET = 1 << TIMELY_TARGET_FLAG_INDEX; +const TIMELY_HEAD = 1 << TIMELY_HEAD_FLAG_INDEX; + +export type ParticipationFlags = { + timelySource: boolean; + timelyTarget: boolean; + timelyHead: boolean; +}; + +export function parseParticipationFlags(flags: number): ParticipationFlags { + return { + timelySource: hasMarkers(flags, TIMELY_SOURCE), + timelyTarget: hasMarkers(flags, TIMELY_TARGET), + timelyHead: hasMarkers(flags, TIMELY_HEAD), + }; +}