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..e5c8eef83679 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, @@ -39,7 +38,14 @@ 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, + inclusionDelays: number[], + flags: number[], + isActiveCurrEpoch: boolean[], + isActivePrevEpoch: boolean[], + 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,12 +121,17 @@ type ValidatorStatus = { inclusionDistance: number; }; -function statusToSummary(status: AttesterStatus): ValidatorStatus { - const flags = parseAttesterFlags(status.flags); +function statusToSummary( + inclusionDelay: number, + flag: number, + isActiveInCurrentEpoch: boolean, + isActiveInPreviousEpoch: boolean +): ValidatorStatus { + const flags = parseAttesterFlags(flag); return { isSlashed: flags.unslashed, - isActiveInCurrentEpoch: status.active, - isActiveInPreviousEpoch: status.active, + isActiveInCurrentEpoch, + isActiveInPreviousEpoch, // TODO: Implement currentEpochEffectiveBalance: 0, @@ -130,7 +141,7 @@ function statusToSummary(status: AttesterStatus): ValidatorStatus { isCurrSourceAttester: flags.currSourceAttester, isCurrTargetAttester: flags.currTargetAttester, isCurrHeadAttester: flags.currHeadAttester, - inclusionDistance: status.inclusionDelay, + inclusionDistance: inclusionDelay, }; } @@ -287,7 +298,7 @@ export function createValidatorMonitor( } }, - registerValidatorStatuses(currentEpoch, statuses, 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; @@ -301,12 +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 status = statuses[index]; - if (status === undefined) { - continue; - } - - const summary = statusToSummary(status); + 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 dc4edf26e084..dfe6bdd8e102 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,12 @@ 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[]; /** * balances array will be populated by processRewardsAndPenalties() and consumed by processEffectiveBalanceUpdates(). @@ -161,6 +164,18 @@ 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 + */ + 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. @@ -168,6 +183,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 @@ -188,9 +218,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; @@ -200,6 +227,32 @@ export function beforeProcessEpoch( const validators = state.validators.getAllReadonlyValues(); const validatorCount = validators.length; + // pre-fill with true (most validators are active) + 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 + // precomputed participation. + // - proposerIndex: number; // -1 when not included by any proposer + // - inclusionDelay: number; + // - flags: number; // bitfield of AttesterFlags + proposerIndices.length = validatorCount; + inclusionDelays.length = validatorCount; + flags.length = validatorCount; + proposerIndices.fill(-1); + inclusionDelays.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(); @@ -207,14 +260,14 @@ export function beforeProcessEpoch( for (let i = 0; i < validatorCount; i++) { const validator = validators[i]; - const status = createAttesterStatus(); + let flag = 0; if (validator.slashed) { if (slashingsEpoch === validator.withdrawableEpoch) { indicesToSlash.push(i); } } else { - status.flags |= FLAG_UNSLASHED; + flag |= FLAG_UNSLASHED; } const {activationEpoch, exitEpoch} = validator; @@ -223,19 +276,24 @@ 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. // TODO: Consider using an array of `eligibleValidatorIndices: number[]` if (isActivePrev || (validator.slashed && prevEpoch + 1 < validator.withdrawableEpoch)) { eligibleValidatorIndices.push(i); - status.flags |= FLAG_ELIGIBLE_ATTESTER; + flag |= FLAG_ELIGIBLE_ATTESTER; } + flags[i] = flag; + if (isActiveCurr) { - status.active = true; totalActiveStakeByIncrement += effectiveBalancesByIncrements[i]; + } else { + isActiveCurrEpoch[i] = false; } // To optimize process_registry_updates(): @@ -278,16 +336,16 @@ 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 ) { indicesToEject.push(i); } - statuses.push(status); - - isActiveNextEpoch.push(isActiveNext); + if (!isActiveNext) { + isActiveNextEpoch[i] = false; + } if (isActiveNext2) { nextEpochShufflingActiveValidatorIndices.push(i); @@ -312,7 +370,9 @@ export function beforeProcessEpoch( if (forkSeq === ForkSeq.phase0) { processPendingAttestations( state as CachedBeaconStatePhase0, - statuses, + proposerIndices, + inclusionDelays, + flags, (state as CachedBeaconStatePhase0).previousEpochAttestations.getAllReadonly(), prevEpoch, FLAG_PREV_SOURCE_ATTESTER, @@ -321,7 +381,9 @@ export function beforeProcessEpoch( ); processPendingAttestations( state as CachedBeaconStatePhase0, - statuses, + proposerIndices, + inclusionDelays, + flags, (state as CachedBeaconStatePhase0).currentEpochAttestations.getAllReadonly(), currentEpoch, FLAG_CURR_SOURCE_ATTESTER, @@ -330,23 +392,15 @@ 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]; - } - } - 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) { - // FLAG_PREV are indexes [3,4,5], so shift by 3 - status.flags |= 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); } } @@ -361,19 +415,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; } } @@ -421,8 +475,12 @@ export function beforeProcessEpoch( nextEpochShufflingActiveValidatorIndices, // to be updated in processEffectiveBalanceUpdates nextEpochTotalActiveBalanceByIncrement: 0, + isActivePrevEpoch, + isActiveCurrEpoch, isActiveNextEpoch, - statuses, + proposerIndices, + inclusionDelays, + 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..dd69738a55b5 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, proposerIndices, inclusionDelays} = cache; + const validatorCount = flags.length; const rewards = newZeroedArray(validatorCount); const penalties = newZeroedArray(validatorCount); @@ -77,12 +78,11 @@ 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++) { + for (let i = 0; i < flags.length; i++) { + const flag = flags[i]; const effectiveBalanceIncrement = effectiveBalanceIncrements[i]; const effectiveBalance = effectiveBalanceIncrement * EFFECTIVE_BALANCE_INCREMENT; - const status = statuses[i]; let rewardItem = rewardPnaltyItemCache.get(effectiveBalanceIncrement); if (!rewardItem) { @@ -121,14 +121,14 @@ export function getAttestationDeltas( } = rewardItem; // inclusion speed bonus - if (hasMarkers(status.flags, FLAG_PREV_SOURCE_ATTESTER_OR_UNSLASHED)) { - rewards[status.proposerIndex] += proposerReward; - rewards[i] += Math.floor(maxAttesterReward / status.inclusionDelay); + if (hasMarkers(flag, FLAG_PREV_SOURCE_ATTESTER_OR_UNSLASHED)) { + rewards[proposerIndices[i]] += proposerReward; + rewards[i] += Math.floor(maxAttesterReward / inclusionDelays[i]); } - 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 +137,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 +146,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 +158,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..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` 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,9 @@ import {computeStartSlotAtEpoch, getBlockRootAtSlot, AttesterStatus} from "../ut */ export function processPendingAttestations( state: CachedBeaconStatePhase0, - statuses: AttesterStatus[], + proposerIndices: number[], + inclusionDelays: number[], + flags: number[], attestations: phase0.PendingAttestation[], epoch: Epoch, sourceFlag: number, @@ -53,21 +55,19 @@ 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; } } } 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..12cec46d9a49 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 = { @@ -21,7 +20,14 @@ export type BeaconStateTransitionMetrics = { postStateBalancesNodesPopulatedHit: Gauge; postStateValidatorsNodesPopulatedMiss: Gauge; postStateValidatorsNodesPopulatedHit: Gauge; - registerValidatorStatuses: (currentEpoch: Epoch, statuses: AttesterStatus[], balances?: number[]) => void; + registerValidatorStatuses: ( + currentEpoch: Epoch, + inclusionDelays: number[], + flags: number[], + isActiveCurrEpoch: boolean[], + isActivePrevEpoch: boolean[], + balances?: number[] + ) => void; }; export function onStateCloneMetrics( diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 7602f4d9acc2..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, statuses, balances} = epochTransitionCache; - metrics?.registerValidatorStatuses(currentEpoch, statuses, balances); + const {currentEpoch, inclusionDelays, flags, isActiveCurrEpoch, isActivePrevEpoch, balances} = + epochTransitionCache; + metrics?.registerValidatorStatuses( + currentEpoch, + inclusionDelays, + flags, + isActiveCurrEpoch, + isActivePrevEpoch, + balances + ); postState.slot++; diff --git a/packages/state-transition/src/util/attesterStatus.ts b/packages/state-transition/src/util/attesterStatus.ts index 4f746615cbec..effacef47e06 100644 --- a/packages/state-transition/src/util/attesterStatus.ts +++ b/packages/state-transition/src/util/attesterStatus.ts @@ -1,45 +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; - -/** - * 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 = { - flags: number; - proposerIndex: number; // -1 when not included by any proposer - inclusionDelay: number; - active: boolean; -}; - -export function createAttesterStatus(): AttesterStatus { - return { - flags: 0, - proposerIndex: -1, - inclusionDelay: 0, - active: false, - }; -} - export function hasMarkers(flags: number, markers: number): boolean { return (flags & markers) === markers; } @@ -81,6 +63,11 @@ export function toAttesterFlags(flagsObj: AttesterFlags): number { 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; 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/array.test.ts b/packages/state-transition/test/perf/epoch/array.test.ts new file mode 100644 index 000000000000..eafd349e982f --- /dev/null +++ b/packages/state-transition/test/perf/epoch/array.test.ts @@ -0,0 +1,60 @@ +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 - length ${N}`, + fn: () => { + new Array(N).fill(0); + for (let i = 0; i < N; i++) { + void 0; + } + }, + }); + itBench({ + id: `Array push - length ${N}`, + fn: () => { + const arr: boolean[] = []; + for (let i = 0; i < N; i++) { + arr.push(true); + } + }, + }); + itBench({ + id: "Array.get", + runsFactor: N, + beforeEach: () => { + return new Array(N).fill(8); + }, + fn: (arr) => { + for (let i = 0; i < N; i++) { + arr[N - 1]; + } + }, + }); + itBench({ + id: "Uint8Array.get", + runsFactor: N, + beforeEach: () => { + return new Uint8Array(N); + }, + fn: (arr) => { + for (let i = 0; i < N; i++) { + arr[N - 1]; + } + }, + }); +}); diff --git a/packages/state-transition/test/perf/epoch/utilPhase0.ts b/packages/state-transition/test/perf/epoch/utilPhase0.ts index f73399ee108f..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,16 +13,18 @@ export function generateBalanceDeltasEpochTransitionCache( ): EpochTransitionCache { const vc = state.validators.length; - const statuses = generateStatuses(state.validators.length, flagFactors); + const {proposerIndices, inclusionDelays, 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, + proposerIndices, + inclusionDelays, + flags, eligibleValidatorIndices, totalActiveStakeByIncrement: vc, baseRewardPerIncrement: 726, @@ -45,19 +41,21 @@ export function generateBalanceDeltasEpochTransitionCache( export type FlagFactors = Record | number; -function generateStatuses(vc: number, flagFactors: FlagFactors): AttesterStatus[] { +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] = { - flags: flagFactors, - proposerIndex: i % totalProposers, - inclusionDelay: 1 + (i % 4), - active: true, - }; + proposerIndices[i] = i % totalProposers; + inclusionDelays[i] = 1 + (i % 4); + flags[i] = flagFactors; } else { // Use a factor to set some validators to this flag const flagsObj: AttesterFlags = { @@ -70,14 +68,11 @@ function generateStatuses(vc: number, flagFactors: FlagFactors): AttesterStatus[ unslashed: i < vc * flagFactors.unslashed, // 6 eligibleAttester: i < vc * flagFactors.eligibleAttester, // 7 }; - statuses[i] = { - flags: toAttesterFlags(flagsObj), - proposerIndex: i % totalProposers, - inclusionDelay: 1 + (i % 4), - active: true, - }; + proposerIndices[i] = i % totalProposers; + inclusionDelays[i] = 1 + (i % 4); + flags[i] = toAttesterFlags(flagsObj); } } - return statuses; + return {proposerIndices, inclusionDelays, flags}; }