import {PublicKey} from "@chainsafe/blst"; import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map"; import {BeaconConfig, ChainConfig, createBeaconConfig} from "@lodestar/config"; import { ATTESTATION_SUBNET_COUNT, DOMAIN_BEACON_PROPOSER, EFFECTIVE_BALANCE_INCREMENT, FAR_FUTURE_EPOCH, ForkSeq, GENESIS_EPOCH, PROPOSER_WEIGHT, SLOTS_PER_EPOCH, WEIGHT_DENOMINATOR, } from "@lodestar/params"; import { Attestation, BLSSignature, CommitteeIndex, Epoch, IndexedAttestation, RootHex, Slot, SubnetID, SyncPeriod, ValidatorIndex, gloas, } from "@lodestar/types"; import {LodestarError} from "@lodestar/utils"; import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; import { EpochShuffling, calculateDecisionRoot, calculateShufflingDecisionRoot, computeEpochShuffling, } from "../util/epochShuffling.js"; import { computeActivationExitEpoch, computeEpochAtSlot, computeProposers, computeSyncPeriodAtEpoch, getActivationChurnLimit, getChurnLimit, getSeed, isActiveValidator, isAggregatorFromCommitteeLength, naiveGetPayloadTimlinessCommitteeIndices, } from "../util/index.js"; import { AttesterDuty, calculateCommitteeAssignments, getAttestingIndices, getBeaconCommittees, getIndexedAttestation, } from "../util/shuffling.js"; import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js"; import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js"; import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js"; import {EpochTransitionCache} from "./epochTransitionCache.js"; import {Index2PubkeyCache, syncPubkeys} from "./pubkeyCache.js"; import {CachedBeaconStateAllForks, CachedBeaconStateFulu} from "./stateCache.js"; import { SyncCommitteeCache, SyncCommitteeCacheEmpty, computeSyncCommitteeCache, getSyncCommitteeCache, } from "./syncCommitteeCache.js"; import {BeaconStateAllForks, BeaconStateAltair, BeaconStateGloas, ShufflingGetter} from "./types.js"; /** `= PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)` */ export const PROPOSER_WEIGHT_FACTOR = PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT); export type EpochCacheImmutableData = { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; }; export type EpochCacheOpts = { skipSyncCommitteeCache?: boolean; skipSyncPubkeys?: boolean; shufflingGetter?: ShufflingGetter; }; /** Defers computing proposers by persisting only the seed, and dropping it once indexes are computed */ type ProposersDeferred = {computed: false; seed: Uint8Array} | {computed: true; indexes: ValidatorIndex[]}; /** * EpochCache is the parent object of: * - Any data-structures not part of the spec'ed BeaconState * - Necessary to only compute data once * - Must be kept at all times through an epoch * * The performance gains with EpochCache are fundamental for the BeaconNode to be able to participate in a * production network with 100_000s of validators. In summary, it contains: * * Expensive data constant through the epoch: * - pubkey cache * - proposer indexes * - shufflings * - sync committee indexed * Counters (maybe) mutated through the epoch: * - churnLimit * - exitQueueEpoch * - exitQueueChurn * Time data faster than recomputing from the state: * - epoch * - syncPeriod **/ export class EpochCache { config: BeaconConfig; /** * Unique globally shared pubkey registry. There should only exist one for the entire application. * * $VALIDATOR_COUNT x 192 char String -> Number Map */ pubkey2index: PubkeyIndexMap; /** * Unique globally shared pubkey registry. There should only exist one for the entire application. * * $VALIDATOR_COUNT x BLST deserialized pubkey (Jacobian coordinates) */ index2pubkey: Index2PubkeyCache; /** * Indexes of the block proposers for the current epoch. * For pre-fulu, this is computed and cached from the current shuffling. * For post-fulu, this is copied from the state.proposerLookahead. * * 32 x Number */ proposers: ValidatorIndex[]; /** Proposers for previous epoch, initialized to null in first epoch */ proposersPrevEpoch: ValidatorIndex[] | null; /** * The next proposer seed is only used in the getBeaconProposersNextEpoch call. It cannot be moved into * getBeaconProposersNextEpoch because it needs state as input and all data needed by getBeaconProposersNextEpoch * should be in the epoch context. * * For pre-fulu, this is lazily computed from the next epoch's shuffling. * For post-fulu, this is copied from the state.proposerLookahead. */ proposersNextEpoch: ProposersDeferred; /** * Epoch decision roots to look up correct shuffling from the Shuffling Cache */ previousDecisionRoot: RootHex; currentDecisionRoot: RootHex; nextDecisionRoot: RootHex; /** * Shuffling of validator indexes. Immutable through the epoch, then it's replaced entirely. * Note: Per spec definition, shuffling will always be defined. They are never called before loadState() * * $VALIDATOR_COUNT x Number */ previousShuffling: EpochShuffling; /** Same as previousShuffling */ currentShuffling: EpochShuffling; /** Same as previousShuffling */ nextShuffling: EpochShuffling; /** * Cache nextActiveIndices so that in afterProcessEpoch the next shuffling can be build synchronously * in case it is not built or the ShufflingCache is not available */ nextActiveIndices: Uint32Array; /** * Effective balances, for altair processAttestations() */ effectiveBalanceIncrements: EffectiveBalanceIncrements; /** * Total state.slashings by increment, for processSlashing() */ totalSlashingsByIncrement: number; syncParticipantReward: number; syncProposerReward: number; /** * Update freq: once per epoch after `process_effective_balance_updates()` */ baseRewardPerIncrement: number; /** * Total active balance for current epoch, to be used instead of getTotalBalance() */ totalActiveBalanceIncrements: number; /** * Rate at which validators can enter or leave the set per epoch. Depends only on activeIndexes, so it does not * change through the epoch. It's used in initiateValidatorExit(). Must be update after changing active indexes. */ churnLimit: number; /** * Fork limited actual activationChurnLimit */ activationChurnLimit: number; /** * Closest epoch with available churn for validators to exit at. May be updated every block as validators are * initiateValidatorExit(). This value may vary on each fork of the state. * * NOTE: Changes block to block * NOTE: No longer used by initiateValidatorExit post-electra */ exitQueueEpoch: Epoch; /** * Number of validators initiating an exit at exitQueueEpoch. May be updated every block as validators are * initiateValidatorExit(). This value may vary on each fork of the state. * * NOTE: Changes block to block * NOTE: No longer used by initiateValidatorExit post-electra */ exitQueueChurn: number; /** * Total cumulative balance increments through epoch for current target. * Required for unrealized checkpoints issue pull-up tips N+1. Otherwise must run epoch transition every block * This value is equivalent to: * - Forward current state to end-of-epoch * - Run beforeProcessEpoch * - epochTransitionCache.currEpochUnslashedTargetStakeByIncrement */ currentTargetUnslashedBalanceIncrements: number; /** * Total cumulative balance increments through epoch for previous target. * Required for unrealized checkpoints issue pull-up tips N+1. Otherwise must run epoch transition every block * This value is equivalent to: * - Forward current state to end-of-epoch * - Run beforeProcessEpoch * - epochTransitionCache.prevEpochUnslashedStake.targetStakeByIncrement */ previousTargetUnslashedBalanceIncrements: number; /** TODO: Indexed SyncCommitteeCache */ currentSyncCommitteeIndexed: SyncCommitteeCache; /** TODO: Indexed SyncCommitteeCache */ nextSyncCommitteeIndexed: SyncCommitteeCache; // TODO GLOAS: See if we need to cached PTC for prev/next epoch // PTC for current epoch payloadTimelinessCommittee: ValidatorIndex[][]; // TODO: Helper stats syncPeriod: SyncPeriod; epoch: Epoch; get nextEpoch(): Epoch { return this.epoch + 1; } constructor(data: { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; proposers: number[]; proposersPrevEpoch: number[] | null; proposersNextEpoch: ProposersDeferred; previousDecisionRoot: RootHex; currentDecisionRoot: RootHex; nextDecisionRoot: RootHex; previousShuffling: EpochShuffling; currentShuffling: EpochShuffling; nextShuffling: EpochShuffling; nextActiveIndices: Uint32Array; effectiveBalanceIncrements: EffectiveBalanceIncrements; totalSlashingsByIncrement: number; syncParticipantReward: number; syncProposerReward: number; baseRewardPerIncrement: number; totalActiveBalanceIncrements: number; churnLimit: number; activationChurnLimit: number; exitQueueEpoch: Epoch; exitQueueChurn: number; currentTargetUnslashedBalanceIncrements: number; previousTargetUnslashedBalanceIncrements: number; currentSyncCommitteeIndexed: SyncCommitteeCache; nextSyncCommitteeIndexed: SyncCommitteeCache; payloadTimelinessCommittee: ValidatorIndex[][]; epoch: Epoch; syncPeriod: SyncPeriod; }) { this.config = data.config; this.pubkey2index = data.pubkey2index; this.index2pubkey = data.index2pubkey; this.proposers = data.proposers; this.proposersPrevEpoch = data.proposersPrevEpoch; this.proposersNextEpoch = data.proposersNextEpoch; this.previousDecisionRoot = data.previousDecisionRoot; this.currentDecisionRoot = data.currentDecisionRoot; this.nextDecisionRoot = data.nextDecisionRoot; this.previousShuffling = data.previousShuffling; this.currentShuffling = data.currentShuffling; this.nextShuffling = data.nextShuffling; this.nextActiveIndices = data.nextActiveIndices; this.effectiveBalanceIncrements = data.effectiveBalanceIncrements; this.totalSlashingsByIncrement = data.totalSlashingsByIncrement; this.syncParticipantReward = data.syncParticipantReward; this.syncProposerReward = data.syncProposerReward; this.baseRewardPerIncrement = data.baseRewardPerIncrement; this.totalActiveBalanceIncrements = data.totalActiveBalanceIncrements; this.churnLimit = data.churnLimit; this.activationChurnLimit = data.activationChurnLimit; this.exitQueueEpoch = data.exitQueueEpoch; this.exitQueueChurn = data.exitQueueChurn; this.currentTargetUnslashedBalanceIncrements = data.currentTargetUnslashedBalanceIncrements; this.previousTargetUnslashedBalanceIncrements = data.previousTargetUnslashedBalanceIncrements; this.currentSyncCommitteeIndexed = data.currentSyncCommitteeIndexed; this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed; this.payloadTimelinessCommittee = data.payloadTimelinessCommittee; this.epoch = data.epoch; this.syncPeriod = data.syncPeriod; } /** * Create an epoch cache * @param state a finalized beacon state. Passing in unfinalized state may cause unexpected behaviour * * SLOW CODE - 🐢 */ static createFromState( state: BeaconStateAllForks, {config, pubkey2index, index2pubkey}: EpochCacheImmutableData, opts?: EpochCacheOpts ): EpochCache { const currentEpoch = computeEpochAtSlot(state.slot); const isGenesis = currentEpoch === GENESIS_EPOCH; const previousEpoch = isGenesis ? GENESIS_EPOCH : currentEpoch - 1; const nextEpoch = currentEpoch + 1; let totalActiveBalanceIncrements = 0; let exitQueueEpoch = computeActivationExitEpoch(currentEpoch); let exitQueueChurn = 0; const validators = state.validators.getAllReadonlyValues(); const validatorCount = validators.length; // syncPubkeys here to ensure EpochCacheImmutableData is popualted before computing the rest of caches // - computeSyncCommitteeCache() needs a fully populated pubkey2index cache if (!opts?.skipSyncPubkeys) { syncPubkeys(validators, pubkey2index, index2pubkey); } const effectiveBalanceIncrements = getEffectiveBalanceIncrementsWithLen(validatorCount); const totalSlashingsByIncrement = getTotalSlashingsByIncrement(state); const previousActiveIndicesAsNumberArray: ValidatorIndex[] = []; const currentActiveIndicesAsNumberArray: ValidatorIndex[] = []; const nextActiveIndicesAsNumberArray: ValidatorIndex[] = []; // BeaconChain could provide a shuffling getter to avoid re-computing shuffling every epoch // in that case, we don't need to compute shufflings again const shufflingGetter = opts?.shufflingGetter; const previousDecisionRoot = calculateShufflingDecisionRoot(config, state, previousEpoch); const cachedPreviousShuffling = shufflingGetter?.(previousEpoch, previousDecisionRoot); const currentDecisionRoot = calculateShufflingDecisionRoot(config, state, currentEpoch); const cachedCurrentShuffling = shufflingGetter?.(currentEpoch, currentDecisionRoot); const nextDecisionRoot = calculateShufflingDecisionRoot(config, state, nextEpoch); const cachedNextShuffling = shufflingGetter?.(nextEpoch, nextDecisionRoot); for (let i = 0; i < validatorCount; i++) { const validator = validators[i]; // Note: Not usable for fork-choice balances since in-active validators are not zero'ed effectiveBalanceIncrements[i] = Math.floor(validator.effectiveBalance / EFFECTIVE_BALANCE_INCREMENT); // Collect active indices for each epoch to compute shufflings if (cachedPreviousShuffling == null && isActiveValidator(validator, previousEpoch)) { previousActiveIndicesAsNumberArray.push(i); } if (isActiveValidator(validator, currentEpoch)) { if (cachedCurrentShuffling == null) { currentActiveIndicesAsNumberArray.push(i); } // We track totalActiveBalanceIncrements as ETH to fit total network balance in a JS number (53 bits) totalActiveBalanceIncrements += effectiveBalanceIncrements[i]; } if (cachedNextShuffling == null && isActiveValidator(validator, nextEpoch)) { nextActiveIndicesAsNumberArray.push(i); } const {exitEpoch} = validator; if (exitEpoch !== FAR_FUTURE_EPOCH) { if (exitEpoch > exitQueueEpoch) { exitQueueEpoch = exitEpoch; exitQueueChurn = 1; } else if (exitEpoch === exitQueueEpoch) { exitQueueChurn += 1; } } } // Spec: `EFFECTIVE_BALANCE_INCREMENT` Gwei minimum to avoid divisions by zero // 1 = 1 unit of EFFECTIVE_BALANCE_INCREMENT if (totalActiveBalanceIncrements < 1) { totalActiveBalanceIncrements = 1; } else if (totalActiveBalanceIncrements >= Number.MAX_SAFE_INTEGER) { throw Error("totalActiveBalanceIncrements >= Number.MAX_SAFE_INTEGER. MAX_EFFECTIVE_BALANCE is too low."); } const nextActiveIndices = new Uint32Array(nextActiveIndicesAsNumberArray); // Use cached shufflings if available, otherwise compute const currentShuffling = cachedCurrentShuffling ?? computeEpochShuffling(state, new Uint32Array(currentActiveIndicesAsNumberArray), currentEpoch); const previousShuffling = cachedPreviousShuffling ?? (isGenesis ? currentShuffling : computeEpochShuffling(state, new Uint32Array(previousActiveIndicesAsNumberArray), previousEpoch)); const nextShuffling = cachedNextShuffling ?? computeEpochShuffling(state, nextActiveIndices, nextEpoch); const currentProposerSeed = getSeed(state, currentEpoch, DOMAIN_BEACON_PROPOSER); let proposers: number[]; if (currentEpoch >= config.FULU_FORK_EPOCH) { // Overwrite proposers with state.proposerLookahead proposers = (state as CachedBeaconStateFulu).proposerLookahead.getAll().slice(0, SLOTS_PER_EPOCH); } else { // We need to calculate Pre-fulu // Allow to create CachedBeaconState for empty states, or no active validators proposers = currentShuffling.activeIndices.length > 0 ? computeProposers( config.getForkSeqAtEpoch(currentEpoch), currentProposerSeed, currentShuffling, effectiveBalanceIncrements ) : []; } const proposersNextEpoch: ProposersDeferred = { computed: false, seed: getSeed(state, nextEpoch, DOMAIN_BEACON_PROPOSER), }; // Only after altair, compute the indices of the current sync committee const afterAltairFork = currentEpoch >= config.ALTAIR_FORK_EPOCH; // Values syncParticipantReward, syncProposerReward, baseRewardPerIncrement are only used after altair. // However, since they are very cheap to compute they are computed always to simplify upgradeState function. const syncParticipantReward = computeSyncParticipantReward(totalActiveBalanceIncrements); const syncProposerReward = Math.floor(syncParticipantReward * PROPOSER_WEIGHT_FACTOR); const baseRewardPerIncrement = computeBaseRewardPerIncrement(totalActiveBalanceIncrements); let currentSyncCommitteeIndexed: SyncCommitteeCache; let nextSyncCommitteeIndexed: SyncCommitteeCache; // Allow to skip populating sync committee for initializeBeaconStateFromEth1() if (afterAltairFork && !opts?.skipSyncCommitteeCache) { const altairState = state as BeaconStateAltair; currentSyncCommitteeIndexed = computeSyncCommitteeCache(altairState.currentSyncCommittee, pubkey2index); nextSyncCommitteeIndexed = computeSyncCommitteeCache(altairState.nextSyncCommittee, pubkey2index); } else { currentSyncCommitteeIndexed = new SyncCommitteeCacheEmpty(); nextSyncCommitteeIndexed = new SyncCommitteeCacheEmpty(); } // Compute PTC for this epoch let payloadTimelinessCommittee: ValidatorIndex[][] = []; if (currentEpoch >= config.GLOAS_FORK_EPOCH) { payloadTimelinessCommittee = naiveGetPayloadTimlinessCommitteeIndices( state as BeaconStateGloas, currentShuffling, effectiveBalanceIncrements, currentEpoch ); } // Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute everytime the // active validator indices set changes in size. Validators change active status only when: // - validator.activation_epoch is set. Only changes in process_registry_updates() if validator can be activated. If // the value changes it will be set to `epoch + 1 + MAX_SEED_LOOKAHEAD`. // - validator.exit_epoch is set. Only changes in initiate_validator_exit() if validator exits. If the value changes, // it will be set to at least `epoch + 1 + MAX_SEED_LOOKAHEAD`. // ``` // is_active_validator = validator.activation_epoch <= epoch < validator.exit_epoch // ``` // So the returned value of is_active_validator(epoch) is guaranteed to not change during `MAX_SEED_LOOKAHEAD` epochs. // // activeIndices size is dependent on the state epoch. The epoch is advanced after running the epoch transition, and // the first block of the epoch process_block() call. So churnLimit must be computed at the end of the before epoch // transition and the result is valid until the end of the next epoch transition const churnLimit = getChurnLimit(config, currentShuffling.activeIndices.length); const activationChurnLimit = getActivationChurnLimit( config, config.getForkSeq(state.slot), currentShuffling.activeIndices.length ); if (exitQueueChurn >= churnLimit) { exitQueueEpoch += 1; exitQueueChurn = 0; } // TODO: describe issue. Compute progressive target balances // Compute balances from zero, note this state could be mid-epoch so target balances != 0 let previousTargetUnslashedBalanceIncrements = 0; let currentTargetUnslashedBalanceIncrements = 0; if (config.getForkSeq(state.slot) >= ForkSeq.altair) { const {previousEpochParticipation, currentEpochParticipation} = state as BeaconStateAltair; previousTargetUnslashedBalanceIncrements = sumTargetUnslashedBalanceIncrements( previousEpochParticipation.getAll(), previousEpoch, validators ); currentTargetUnslashedBalanceIncrements = sumTargetUnslashedBalanceIncrements( currentEpochParticipation.getAll(), currentEpoch, validators ); } return new EpochCache({ config, pubkey2index, index2pubkey, proposers, // On first epoch, set to null to prevent unnecessary work since this is only used for metrics proposersPrevEpoch: null, proposersNextEpoch, previousDecisionRoot, currentDecisionRoot, nextDecisionRoot, previousShuffling, currentShuffling, nextShuffling, nextActiveIndices, effectiveBalanceIncrements, totalSlashingsByIncrement, syncParticipantReward, syncProposerReward, baseRewardPerIncrement, totalActiveBalanceIncrements, churnLimit, activationChurnLimit, exitQueueEpoch, exitQueueChurn, previousTargetUnslashedBalanceIncrements, currentTargetUnslashedBalanceIncrements, currentSyncCommitteeIndexed, nextSyncCommitteeIndexed, payloadTimelinessCommittee: payloadTimelinessCommittee, epoch: currentEpoch, syncPeriod: computeSyncPeriodAtEpoch(currentEpoch), }); } /** * Copies a given EpochCache while avoiding copying its immutable parts. */ clone(): EpochCache { // warning: pubkey cache is not copied, it is shared, as eth1 is not expected to reorder validators. // Shallow copy all data from current epoch context to the next // All data is completely replaced, or only-appended return new EpochCache({ config: this.config, // Common append-only structures shared with all states, no need to clone pubkey2index: this.pubkey2index, index2pubkey: this.index2pubkey, // Immutable data proposers: this.proposers, proposersPrevEpoch: this.proposersPrevEpoch, proposersNextEpoch: this.proposersNextEpoch, previousDecisionRoot: this.previousDecisionRoot, currentDecisionRoot: this.currentDecisionRoot, nextDecisionRoot: this.nextDecisionRoot, previousShuffling: this.previousShuffling, currentShuffling: this.currentShuffling, nextShuffling: this.nextShuffling, nextActiveIndices: this.nextActiveIndices, // Uint8Array, requires cloning, but it is cloned only when necessary before an epoch transition // See EpochCache.beforeEpochTransition() effectiveBalanceIncrements: this.effectiveBalanceIncrements, totalSlashingsByIncrement: this.totalSlashingsByIncrement, // Basic types (numbers) cloned implicitly syncParticipantReward: this.syncParticipantReward, syncProposerReward: this.syncProposerReward, baseRewardPerIncrement: this.baseRewardPerIncrement, totalActiveBalanceIncrements: this.totalActiveBalanceIncrements, churnLimit: this.churnLimit, activationChurnLimit: this.activationChurnLimit, exitQueueEpoch: this.exitQueueEpoch, exitQueueChurn: this.exitQueueChurn, previousTargetUnslashedBalanceIncrements: this.previousTargetUnslashedBalanceIncrements, currentTargetUnslashedBalanceIncrements: this.currentTargetUnslashedBalanceIncrements, currentSyncCommitteeIndexed: this.currentSyncCommitteeIndexed, nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed, payloadTimelinessCommittee: this.payloadTimelinessCommittee, epoch: this.epoch, syncPeriod: this.syncPeriod, }); } /** * Called to re-use information, such as the shuffling of the next epoch, after transitioning into a * new epoch. Also handles pre-computation of values that may change during the upcoming epoch and * that get used in the following epoch transition. Often those pre-computations are not used by the * chain but are courtesy values that are served via the API for epoch look ahead of duties. * * Steps for afterProcessEpoch * 1) update previous/current/next values of cached items * * At fork boundary, this runs pre-fork logic and it happens before `upgradeState*` is called. */ afterProcessEpoch(state: CachedBeaconStateAllForks, epochTransitionCache: EpochTransitionCache): void { // Because the slot was incremented before entering this function the "next epoch" is actually the "current epoch" // in this context but that is not actually true because the state transition happens in the last 4 seconds of the // epoch. For the context of this function "upcoming epoch" is used to denote the epoch that will begin after this // function returns. The epoch that is "next" once the state transition is complete is referred to as the // epochAfterUpcoming for the same reason to help minimize confusion. const upcomingEpoch = this.nextEpoch; const epochAfterUpcoming = upcomingEpoch + 1; // move current to previous this.previousShuffling = this.currentShuffling; this.previousDecisionRoot = this.currentDecisionRoot; // move next to current this.currentDecisionRoot = this.nextDecisionRoot; this.currentShuffling = this.nextShuffling; // Compute shuffling for epoch n+2 // // Post-Fulu (EIP-7917), the beacon state includes a `proposer_lookahead` field that stores // proposer indices for MIN_SEED_LOOKAHEAD + 1 epochs ahead (2 epochs with MIN_SEED_LOOKAHEAD=1). // At each epoch boundary, processProposerLookahead() shifts out the current epoch's proposers // and appends new proposers for epoch n + MIN_SEED_LOOKAHEAD + 1 (i.e., epoch n+2). // // processProposerLookahead() already computes the n+2 shuffling and stores it in // epochTransitionCache.nextShuffling. Reuse it here to avoid duplicate computation. // Pre-Fulu, we need to compute it here since processProposerLookahead doesn't run. // // See: https://eips.ethereum.org/EIPS/eip-7917 this.nextDecisionRoot = calculateDecisionRoot(state, epochAfterUpcoming); this.nextActiveIndices = epochTransitionCache.nextShufflingActiveIndices; this.nextShuffling = epochTransitionCache.nextShuffling ?? computeEpochShuffling(state, this.nextActiveIndices, epochAfterUpcoming); // TODO: DEDUPLICATE from createEpochCache // // Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute every time the // active validator indices set changes in size. Validators change active status only when: // - validator.activation_epoch is set. Only changes in process_registry_updates() if validator can be activated. If // the value changes it will be set to `epoch + 1 + MAX_SEED_LOOKAHEAD`. // - validator.exit_epoch is set. Only changes in initiate_validator_exit() if validator exits. If the value changes, // it will be set to at least `epoch + 1 + MAX_SEED_LOOKAHEAD`. // ``` // is_active_validator = validator.activation_epoch <= epoch < validator.exit_epoch // ``` // So the returned value of is_active_validator(epoch) is guaranteed to not change during `MAX_SEED_LOOKAHEAD` epochs. // // activeIndices size is dependent on the state epoch. The epoch is advanced after running the epoch transition, and // the first block of the epoch process_block() call. So churnLimit must be computed at the end of the before epoch // transition and the result is valid until the end of the next epoch transition this.churnLimit = getChurnLimit(this.config, this.currentShuffling.activeIndices.length); this.activationChurnLimit = getActivationChurnLimit( this.config, this.config.getForkSeq(state.slot), this.currentShuffling.activeIndices.length ); // Maybe advance exitQueueEpoch at the end of the epoch if there haven't been any exists for a while const exitQueueEpoch = computeActivationExitEpoch(upcomingEpoch); if (exitQueueEpoch > this.exitQueueEpoch) { this.exitQueueEpoch = exitQueueEpoch; this.exitQueueChurn = 0; } this.totalActiveBalanceIncrements = epochTransitionCache.nextEpochTotalActiveBalanceByIncrement; if (upcomingEpoch >= this.config.ALTAIR_FORK_EPOCH) { this.syncParticipantReward = computeSyncParticipantReward(this.totalActiveBalanceIncrements); this.syncProposerReward = Math.floor(this.syncParticipantReward * PROPOSER_WEIGHT_FACTOR); this.baseRewardPerIncrement = computeBaseRewardPerIncrement(this.totalActiveBalanceIncrements); } this.previousTargetUnslashedBalanceIncrements = this.currentTargetUnslashedBalanceIncrements; this.currentTargetUnslashedBalanceIncrements = 0; // Advance time units // state.slot is advanced right before calling this function // ``` // postState.slot++; // afterProcessEpoch(postState, epochTransitionCache); // ``` this.epoch = computeEpochAtSlot(state.slot); this.syncPeriod = computeSyncPeriodAtEpoch(this.epoch); } /** * At fork boundary, this runs post-fork logic and it happens after `upgradeState*` is called. */ finalProcessEpoch(state: CachedBeaconStateAllForks): void { // this.epoch was updated at the end of afterProcessEpoch() const upcomingEpoch = this.epoch; const epochAfterUpcoming = upcomingEpoch + 1; this.proposersPrevEpoch = this.proposers; if (upcomingEpoch >= this.config.GLOAS_FORK_EPOCH) { this.payloadTimelinessCommittee = naiveGetPayloadTimlinessCommitteeIndices( state as BeaconStateGloas, this.currentShuffling, this.effectiveBalanceIncrements, upcomingEpoch ); } if (upcomingEpoch >= this.config.FULU_FORK_EPOCH) { // Populate proposer cache with lookahead from state const proposerLookahead = (state as CachedBeaconStateFulu).proposerLookahead.getAll(); this.proposers = proposerLookahead.slice(0, SLOTS_PER_EPOCH); if (proposerLookahead.length >= SLOTS_PER_EPOCH * 2) { this.proposersNextEpoch = { computed: true, indexes: proposerLookahead.slice(SLOTS_PER_EPOCH, SLOTS_PER_EPOCH * 2), }; } else { // This should not happen unless MIN_SEED_LOOKAHEAD is set to 0 // this ensures things don't break if the proposer lookahead is not long enough this.proposersNextEpoch = {computed: false, seed: getSeed(state, epochAfterUpcoming, DOMAIN_BEACON_PROPOSER)}; } } else { // Need to calculate proposers pre-fulu const upcomingProposerSeed = getSeed(state, upcomingEpoch, DOMAIN_BEACON_PROPOSER); // next epoch was moved to current epoch so use current here this.proposers = computeProposers( this.config.getForkSeqAtEpoch(upcomingEpoch), upcomingProposerSeed, this.currentShuffling, this.effectiveBalanceIncrements ); // Only pre-compute the seed since it's very cheap. Do the expensive computeProposers() call only on demand. this.proposersNextEpoch = {computed: false, seed: getSeed(state, epochAfterUpcoming, DOMAIN_BEACON_PROPOSER)}; } } beforeEpochTransition(): void { // Clone (copy) before being mutated in processEffectiveBalanceUpdates // NOTE: Force to use Uint16Array.slice (copy) instead of Buffer.call (not copy) this.effectiveBalanceIncrements = Uint16Array.prototype.slice.call(this.effectiveBalanceIncrements, 0); } /** * Return the beacon committee at slot for index. */ getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array { return this.getBeaconCommittees(slot, [index])[0]; } /** * Return a Uint32Array[] representing committees of indices */ getBeaconCommittees(slot: Slot, indices: CommitteeIndex[]): Uint32Array[] { if (indices.length === 0) { throw new Error("Attempt to get committees without providing CommitteeIndex"); } return getBeaconCommittees(this.getShufflingAtSlot(slot), slot, indices); } getCommitteeCountPerSlot(epoch: Epoch): number { return this.getShufflingAtEpoch(epoch).committeesPerSlot; } /** * Compute the correct subnet for a slot/committee index */ computeSubnetForSlot(slot: number, committeeIndex: number): SubnetID { const slotsSinceEpochStart = slot % SLOTS_PER_EPOCH; const committeesPerSlot = this.getCommitteeCountPerSlot(computeEpochAtSlot(slot)); const committeesSinceEpochStart = committeesPerSlot * slotsSinceEpochStart; return (committeesSinceEpochStart + committeeIndex) % ATTESTATION_SUBNET_COUNT; } /** * Read from proposers instead of state.proposer_lookahead because we set it in `finalProcessEpoch()` * See https://github.com/ethereum/consensus-specs/blob/e9266b2145c09b63ba0039a9f477cfe8a629c78b/specs/fulu/beacon-chain.md#modified-get_beacon_proposer_index */ getBeaconProposer(slot: Slot): ValidatorIndex { const epoch = computeEpochAtSlot(slot); if (epoch !== this.currentShuffling.epoch) { throw new EpochCacheError({ code: EpochCacheErrorCode.PROPOSER_EPOCH_MISMATCH, currentEpoch: this.currentShuffling.epoch, requestedEpoch: epoch, }); } return this.proposers[slot % SLOTS_PER_EPOCH]; } getBeaconProposers(): ValidatorIndex[] { return this.proposers; } getBeaconProposersPrevEpoch(): ValidatorIndex[] | null { return this.proposersPrevEpoch; } /** * We allow requesting proposal duties 1 epoch in the future as in normal network conditions it's possible to predict * the correct shuffling with high probability. While knowing the proposers in advance is not useful for consensus, * users want to know it to plan manteinance and avoid missing block proposals. * * **How to predict future proposers** * * Proposer duties for epoch N are guaranteed to be known at epoch N. Proposer duties depend exclusively on: * 1. seed (from randao_mix): known 2 epochs ahead * 2. active validator set: known 4 epochs ahead * 3. effective balance: not known ahead * * ```python * def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex: * epoch = get_current_epoch(state) * seed = hash(get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + uint_to_bytes(state.slot)) * indices = get_active_validator_indices(state, epoch) * return compute_proposer_index(state, indices, seed) * ``` * * **1**: If `MIN_SEED_LOOKAHEAD = 1` the randao_mix used for the seed is from 2 epochs ago. So at epoch N, the seed * is known and unchangable for duties at epoch N+1 and N+2 for proposer duties. * * ```python * def get_seed(state: BeaconState, epoch: Epoch, domain_type: DomainType) -> Bytes32: * mix = get_randao_mix(state, Epoch(epoch - MIN_SEED_LOOKAHEAD - 1)) * return hash(domain_type + uint_to_bytes(epoch) + mix) * ``` * * **2**: The active validator set can be predicted `MAX_SEED_LOOKAHEAD` in advance due to how activations are * processed. We already compute the active validator set for the next epoch to optimize epoch processing, so it's * reused here. * * **3**: Effective balance is not known ahead of time, but it rarely changes. Even if it changes, only a few * balances are sampled to adjust the probability of the next selection (32 per epoch on average). So to invalidate * the prediction the effective of one of those 32 samples should change and change the random_byte inequality. */ getBeaconProposersNextEpoch(): ValidatorIndex[] { if (!this.proposersNextEpoch.computed) { // this is lazily computed pre-fulu const indexes = computeProposers( this.config.getForkSeqAtEpoch(this.nextEpoch), this.proposersNextEpoch.seed, this.getShufflingAtEpoch(this.nextEpoch), this.effectiveBalanceIncrements ); this.proposersNextEpoch = {computed: true, indexes}; } // this is eagerly computed post-fulu return this.proposersNextEpoch.indexes; } /** * Return the indexed attestation corresponding to ``attestation``. */ getIndexedAttestation(fork: ForkSeq, attestation: Attestation): IndexedAttestation { const shuffling = this.getShufflingAtSlot(attestation.data.slot); return getIndexedAttestation(shuffling, fork, attestation); } /** * Return indices of validators who attestested in `attestation` */ getAttestingIndices(fork: ForkSeq, attestation: Attestation): number[] { const shuffling = this.getShufflingAtSlot(attestation.data.slot); return getAttestingIndices(shuffling, fork, attestation); } getCommitteeAssignments( epoch: Epoch, requestedValidatorIndices: ValidatorIndex[] ): Map { const shuffling = this.getShufflingAtEpoch(epoch); return calculateCommitteeAssignments(shuffling, requestedValidatorIndices); } isAggregator(slot: Slot, index: CommitteeIndex, slotSignature: BLSSignature): boolean { const committee = this.getBeaconCommittee(slot, index); return isAggregatorFromCommitteeLength(committee.length, slotSignature); } /** * Return pubkey given the validator index. */ getPubkey(index: ValidatorIndex): PublicKey | undefined { return this.index2pubkey[index]; } getValidatorIndex(pubkey: Uint8Array): ValidatorIndex | null { return this.pubkey2index.get(pubkey); } addPubkey(index: ValidatorIndex, pubkey: Uint8Array): void { this.pubkey2index.set(pubkey, index); this.index2pubkey[index] = PublicKey.fromBytes(pubkey); // Optimize for aggregation } getShufflingAtSlot(slot: Slot): EpochShuffling { const epoch = computeEpochAtSlot(slot); return this.getShufflingAtEpoch(epoch); } getShufflingAtSlotOrNull(slot: Slot): EpochShuffling | null { const epoch = computeEpochAtSlot(slot); return this.getShufflingAtEpochOrNull(epoch); } getShufflingAtEpoch(epoch: Epoch): EpochShuffling { const shuffling = this.getShufflingAtEpochOrNull(epoch); if (shuffling === null) { if (epoch === this.nextEpoch) { throw new EpochCacheError({ code: EpochCacheErrorCode.NEXT_SHUFFLING_NOT_AVAILABLE, epoch: epoch, decisionRoot: this.getShufflingDecisionRoot(this.nextEpoch), }); } throw new EpochCacheError({ code: EpochCacheErrorCode.COMMITTEE_EPOCH_OUT_OF_RANGE, currentEpoch: this.currentShuffling.epoch, requestedEpoch: epoch, }); } return shuffling; } getShufflingDecisionRoot(epoch: Epoch): RootHex { switch (epoch) { case this.epoch - 1: return this.previousDecisionRoot; case this.epoch: return this.currentDecisionRoot; case this.nextEpoch: return this.nextDecisionRoot; default: throw new EpochCacheError({ code: EpochCacheErrorCode.DECISION_ROOT_EPOCH_OUT_OF_RANGE, currentEpoch: this.epoch, requestedEpoch: epoch, }); } } getShufflingAtEpochOrNull(epoch: Epoch): EpochShuffling | null { switch (epoch) { case this.epoch - 1: return this.previousShuffling; case this.epoch: return this.currentShuffling; case this.nextEpoch: return this.nextShuffling; default: return null; } } /** * Note: The range of slots a validator has to perform duties is off by one. * The previous slot wording means that if your validator is in a sync committee for a period that runs from slot * 100 to 200,then you would actually produce signatures in slot 99 - 199. */ getIndexedSyncCommittee(slot: Slot): SyncCommitteeCache { // See note above for the +1 offset return this.getIndexedSyncCommitteeAtEpoch(computeEpochAtSlot(slot + 1)); } /** * **DO NOT USE FOR GOSSIP VALIDATION**: Sync committee duties are offset by one slot. @see {@link EpochCache.getIndexedSyncCommittee} * * Get indexed sync committee at epoch without offsets */ getIndexedSyncCommitteeAtEpoch(epoch: Epoch): SyncCommitteeCache { switch (computeSyncPeriodAtEpoch(epoch)) { case this.syncPeriod: return this.currentSyncCommitteeIndexed; case this.syncPeriod + 1: return this.nextSyncCommitteeIndexed; default: throw new EpochCacheError({code: EpochCacheErrorCode.NO_SYNC_COMMITTEE, epoch}); } } /** On processSyncCommitteeUpdates rotate next to current and set nextSyncCommitteeIndexed */ rotateSyncCommitteeIndexed(nextSyncCommitteeIndices: Uint32Array): void { this.currentSyncCommitteeIndexed = this.nextSyncCommitteeIndexed; this.nextSyncCommitteeIndexed = getSyncCommitteeCache(nextSyncCommitteeIndices); } /** On phase0 -> altair fork, set both current and nextSyncCommitteeIndexed */ setSyncCommitteesIndexed(nextSyncCommitteeIndices: Uint32Array): void { this.nextSyncCommitteeIndexed = getSyncCommitteeCache(nextSyncCommitteeIndices); this.currentSyncCommitteeIndexed = this.nextSyncCommitteeIndexed; } effectiveBalanceIncrementsSet(index: number, effectiveBalance: number): void { if (this.isPostElectra()) { // TODO: electra // getting length and setting getEffectiveBalanceIncrementsByteLen is not fork safe // so each time we add an index, we should new the Uint8Array to keep it forksafe // one simple optimization could be to increment the length once per block rather // on each add/set // // there could still be some unused length remaining from the prev ELECTRA padding const newLength = index >= this.effectiveBalanceIncrements.length ? index + 1 : this.effectiveBalanceIncrements.length; const effectiveBalanceIncrements = this.effectiveBalanceIncrements; this.effectiveBalanceIncrements = new Uint16Array(newLength); this.effectiveBalanceIncrements.set(effectiveBalanceIncrements, 0); } else { if (index >= this.effectiveBalanceIncrements.length) { // Clone and extend effectiveBalanceIncrements const effectiveBalanceIncrements = this.effectiveBalanceIncrements; this.effectiveBalanceIncrements = new Uint16Array(getEffectiveBalanceIncrementsByteLen(index + 1)); this.effectiveBalanceIncrements.set(effectiveBalanceIncrements, 0); } } this.effectiveBalanceIncrements[index] = Math.floor(effectiveBalance / EFFECTIVE_BALANCE_INCREMENT); } isPostElectra(): boolean { return this.epoch >= this.config.ELECTRA_FORK_EPOCH; } getPayloadTimelinessCommittee(slot: Slot): ValidatorIndex[] { const epoch = computeEpochAtSlot(slot); if (epoch < this.config.GLOAS_FORK_EPOCH) { throw new Error("Payload Timeliness Committee is not available before gloas fork"); } if (epoch === this.epoch) { return this.payloadTimelinessCommittee[slot % SLOTS_PER_EPOCH]; } throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`); } getIndexedPayloadAttestation( slot: Slot, payloadAttestation: gloas.PayloadAttestation ): gloas.IndexedPayloadAttestation { const payloadTimelinessCommittee = this.getPayloadTimelinessCommittee(slot); const attestingIndices = payloadAttestation.aggregationBits.intersectValues(payloadTimelinessCommittee); return { attestingIndices: attestingIndices.sort((a, b) => a - b), data: payloadAttestation.data, signature: payloadAttestation.signature, }; } } function getEffectiveBalanceIncrementsByteLen(validatorCount: number): number { // TODO: Research what's the best number to minimize both memory cost and copy costs return 1024 * Math.ceil(validatorCount / 1024); } export enum EpochCacheErrorCode { COMMITTEE_EPOCH_OUT_OF_RANGE = "EPOCH_CONTEXT_ERROR_COMMITTEE_EPOCH_OUT_OF_RANGE", DECISION_ROOT_EPOCH_OUT_OF_RANGE = "EPOCH_CONTEXT_ERROR_DECISION_ROOT_EPOCH_OUT_OF_RANGE", NEXT_SHUFFLING_NOT_AVAILABLE = "EPOCH_CONTEXT_ERROR_NEXT_SHUFFLING_NOT_AVAILABLE", NO_SYNC_COMMITTEE = "EPOCH_CONTEXT_ERROR_NO_SYNC_COMMITTEE", PROPOSER_EPOCH_MISMATCH = "EPOCH_CONTEXT_ERROR_PROPOSER_EPOCH_MISMATCH", } type EpochCacheErrorType = | { code: EpochCacheErrorCode.COMMITTEE_EPOCH_OUT_OF_RANGE; requestedEpoch: Epoch; currentEpoch: Epoch; } | { code: EpochCacheErrorCode.DECISION_ROOT_EPOCH_OUT_OF_RANGE; requestedEpoch: Epoch; currentEpoch: Epoch; } | { code: EpochCacheErrorCode.NEXT_SHUFFLING_NOT_AVAILABLE; epoch: Epoch; decisionRoot: RootHex; } | { code: EpochCacheErrorCode.NO_SYNC_COMMITTEE; epoch: Epoch; } | { code: EpochCacheErrorCode.PROPOSER_EPOCH_MISMATCH; requestedEpoch: Epoch; currentEpoch: Epoch; }; export class EpochCacheError extends LodestarError {} export function createEmptyEpochCacheImmutableData( chainConfig: ChainConfig, state: Pick ): EpochCacheImmutableData { return { config: createBeaconConfig(chainConfig, state.genesisValidatorsRoot), // This is a test state, there's no need to have a global shared cache of keys pubkey2index: new PubkeyIndexMap(), index2pubkey: [], }; }