From bb767410505e610e0b9a3e43482e4ec8005f4eb5 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 22 Nov 2023 14:38:48 +0700 Subject: [PATCH 1/7] feat: update slashingPenalties in processRewardsAndPenalties --- packages/state-transition/src/epoch/index.ts | 8 +++-- .../src/epoch/processRewardsAndPenalties.ts | 8 +++-- .../src/epoch/processSlashings.ts | 31 ++++++++++++++++--- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/state-transition/src/epoch/index.ts b/packages/state-transition/src/epoch/index.ts index 80b6f83f4b8b..51821923cd00 100644 --- a/packages/state-transition/src/epoch/index.ts +++ b/packages/state-transition/src/epoch/index.ts @@ -47,9 +47,13 @@ export function processEpoch(fork: ForkSeq, state: CachedBeaconStateAllForks, ca if (fork >= ForkSeq.altair) { processInactivityUpdates(state as CachedBeaconStateAltair, cache); } - processRewardsAndPenalties(state, cache); + // processRewardsAndPenalties() is 2nd step on the specs, we optimize to do it + // after processSlashings() to update balances only once + // processRewardsAndPenalties(state, cache); processRegistryUpdates(state, cache); - processSlashings(state, cache); + // accumulate slashing penalties and only update balances once in processRewardsAndPenalties() + const slashingPenalties = processSlashings(state, cache, false); + processRewardsAndPenalties(state, cache, slashingPenalties); processEth1DataReset(state, cache); processEffectiveBalanceUpdates(state, cache); processSlashingsReset(state, cache); diff --git a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts index 20cf8597298b..61680b81002a 100644 --- a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts @@ -14,7 +14,11 @@ import {getRewardsAndPenaltiesAltair} from "./getRewardsAndPenalties.js"; * * PERF: Cost = 'proportional' to $VALIDATOR_COUNT. Extra work is done per validator the more status flags are set */ -export function processRewardsAndPenalties(state: CachedBeaconStateAllForks, cache: EpochTransitionCache): void { +export function processRewardsAndPenalties( + state: CachedBeaconStateAllForks, + cache: EpochTransitionCache, + slashingPenalties: number[] = [] +): void { // No rewards are applied at the end of `GENESIS_EPOCH` because rewards are for work done in the previous epoch if (cache.currentEpoch === GENESIS_EPOCH) { return; @@ -24,7 +28,7 @@ export function processRewardsAndPenalties(state: CachedBeaconStateAllForks, cac const balances = state.balances.getAll(); for (let i = 0, len = rewards.length; i < len; i++) { - balances[i] += rewards[i] - penalties[i]; + balances[i] += rewards[i] - penalties[i] - (slashingPenalties[i] ?? 0); } // important: do not change state one balance at a time. Set them all at once, constructing the tree in one go diff --git a/packages/state-transition/src/epoch/processSlashings.ts b/packages/state-transition/src/epoch/processSlashings.ts index 456f1ebc31fa..91073eed0e9f 100644 --- a/packages/state-transition/src/epoch/processSlashings.ts +++ b/packages/state-transition/src/epoch/processSlashings.ts @@ -12,6 +12,9 @@ import {CachedBeaconStateAllForks, EpochTransitionCache} from "../types.js"; /** * Update validator registry for validators that activate + exit + * updateBalance is an optimization: + * - For spec test, it's true + * - For processEpoch flow, it's false, i.e to only update balances once in processRewardsAndPenalties() * * PERF: Cost 'proportional' to only validators that are slashed. For mainnet conditions: * - indicesToSlash: max len is 8704. But it's very unlikely since it would require all validators on the same @@ -19,10 +22,14 @@ import {CachedBeaconStateAllForks, EpochTransitionCache} from "../types.js"; * * - On normal mainnet conditions indicesToSlash = 0 */ -export function processSlashings(state: CachedBeaconStateAllForks, cache: EpochTransitionCache): void { +export function processSlashings( + state: CachedBeaconStateAllForks, + cache: EpochTransitionCache, + updateBalance = true +): number[] { // No need to compute totalSlashings if there no index to slash if (cache.indicesToSlash.length === 0) { - return; + return []; } // TODO: have the regular totalBalance in EpochTransitionCache too? const totalBalance = BigInt(cache.totalActiveStakeByIncrement) * BigInt(EFFECTIVE_BALANCE_INCREMENT); @@ -46,10 +53,24 @@ export function processSlashings(state: CachedBeaconStateAllForks, cache: EpochT const {effectiveBalanceIncrements} = state.epochCtx; const adjustedTotalSlashingBalance = bigIntMin(totalSlashings * BigInt(proportionalSlashingMultiplier), totalBalance); const increment = EFFECTIVE_BALANCE_INCREMENT; + const penalties: number[] = []; + const penaltiesByEffectiveBalanceIncrement = new Map(); for (const index of cache.indicesToSlash) { const effectiveBalanceIncrement = effectiveBalanceIncrements[index]; - const penaltyNumerator = BigInt(effectiveBalanceIncrement) * adjustedTotalSlashingBalance; - const penalty = Number(penaltyNumerator / totalBalance) * increment; - decreaseBalance(state, index, penalty); + let penalty = penaltiesByEffectiveBalanceIncrement.get(effectiveBalanceIncrement); + if (penalty === undefined) { + const penaltyNumerator = BigInt(effectiveBalanceIncrement) * adjustedTotalSlashingBalance; + penalty = Number(penaltyNumerator / totalBalance) * increment; + penaltiesByEffectiveBalanceIncrement.set(effectiveBalanceIncrement, penalty); + } + + if (updateBalance) { + // for spec test only + decreaseBalance(state, index, penalty); + } else { + // do it later in processRewardsAndPenalties() + penalties[index] = penalty; + } } + return penalties; } From 20005b8a6d24567a4ada60f4bee676f57716e59f Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Nov 2023 10:16:36 +0700 Subject: [PATCH 2/7] feat: cache totalSlashingsByIncrement for processSlashings --- .../src/block/slashValidator.ts | 3 +- .../state-transition/src/cache/epochCache.ts | 10 ++++ .../src/epoch/processSlashings.ts | 50 ++++++++++++------- .../src/epoch/processSlashingsReset.ts | 10 +++- .../test/perf/epoch/epochAltair.test.ts | 4 +- .../test/perf/epoch/epochCapella.test.ts | 4 +- .../test/perf/epoch/epochPhase0.test.ts | 4 +- 7 files changed, 61 insertions(+), 24 deletions(-) diff --git a/packages/state-transition/src/block/slashValidator.ts b/packages/state-transition/src/block/slashValidator.ts index 7f4811a8f163..b86272a225b2 100644 --- a/packages/state-transition/src/block/slashValidator.ts +++ b/packages/state-transition/src/block/slashValidator.ts @@ -27,7 +27,7 @@ export function slashValidator( whistleblowerIndex?: ValidatorIndex ): void { const {epochCtx} = state; - const epoch = epochCtx.epoch; + const {epoch, effectiveBalanceIncrements} = epochCtx; const validator = state.validators.get(slashedIndex); // TODO: Bellatrix initiateValidatorExit validators.update() with the one below @@ -40,6 +40,7 @@ export function slashValidator( // TODO: could state.slashings be number? const slashingIndex = epoch % EPOCHS_PER_SLASHINGS_VECTOR; state.slashings.set(slashingIndex, state.slashings.get(slashingIndex) + BigInt(effectiveBalance)); + epochCtx.totalSlashingsByIncrement += effectiveBalanceIncrements[slashedIndex]; const minSlashingPenaltyQuotient = fork === ForkSeq.phase0 diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index 0ca09526e7ec..8b63b0285098 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -29,6 +29,7 @@ import { import {computeEpochShuffling, EpochShuffling, getShufflingDecisionBlock} from "../util/epochShuffling.js"; import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js"; import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js"; +import {getTotalSlashingsByIncrement} from "../epoch/processSlashings.js"; import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js"; import {Index2PubkeyCache, PubkeyIndexMap, syncPubkeys} from "./pubkeyCache.js"; import {BeaconStateAllForks, BeaconStateAltair, ShufflingGetter} from "./types.js"; @@ -131,6 +132,10 @@ export class EpochCache { * Effective balances, for altair processAttestations() */ effectiveBalanceIncrements: EffectiveBalanceIncrements; + /** + * Total state.slashings by increment, for processSlashing() + */ + totalSlashingsByIncrement: number; syncParticipantReward: number; syncProposerReward: number; /** @@ -206,6 +211,7 @@ export class EpochCache { currentShuffling: EpochShuffling; nextShuffling: EpochShuffling; effectiveBalanceIncrements: EffectiveBalanceIncrements; + totalSlashingsByIncrement: number; syncParticipantReward: number; syncProposerReward: number; baseRewardPerIncrement: number; @@ -231,6 +237,7 @@ export class EpochCache { this.currentShuffling = data.currentShuffling; this.nextShuffling = data.nextShuffling; this.effectiveBalanceIncrements = data.effectiveBalanceIncrements; + this.totalSlashingsByIncrement = data.totalSlashingsByIncrement; this.syncParticipantReward = data.syncParticipantReward; this.syncProposerReward = data.syncProposerReward; this.baseRewardPerIncrement = data.baseRewardPerIncrement; @@ -277,6 +284,7 @@ export class EpochCache { const validatorCount = validators.length; const effectiveBalanceIncrements = getEffectiveBalanceIncrementsWithLen(validatorCount); + const totalSlashingsByIncrement = getTotalSlashingsByIncrement(state); const previousActiveIndices: ValidatorIndex[] = []; const currentActiveIndices: ValidatorIndex[] = []; const nextActiveIndices: ValidatorIndex[] = []; @@ -425,6 +433,7 @@ export class EpochCache { currentShuffling, nextShuffling, effectiveBalanceIncrements, + totalSlashingsByIncrement, syncParticipantReward, syncProposerReward, baseRewardPerIncrement, @@ -464,6 +473,7 @@ export class EpochCache { // 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, diff --git a/packages/state-transition/src/epoch/processSlashings.ts b/packages/state-transition/src/epoch/processSlashings.ts index 91073eed0e9f..378730fe8ffe 100644 --- a/packages/state-transition/src/epoch/processSlashings.ts +++ b/packages/state-transition/src/epoch/processSlashings.ts @@ -1,4 +1,3 @@ -import {bigIntMin} from "@lodestar/utils"; import { EFFECTIVE_BALANCE_INCREMENT, ForkSeq, @@ -8,7 +7,7 @@ import { } from "@lodestar/params"; import {decreaseBalance} from "../util/index.js"; -import {CachedBeaconStateAllForks, EpochTransitionCache} from "../types.js"; +import {BeaconStateAllForks, CachedBeaconStateAllForks, EpochTransitionCache} from "../types.js"; /** * Update validator registry for validators that activate + exit @@ -16,32 +15,27 @@ import {CachedBeaconStateAllForks, EpochTransitionCache} from "../types.js"; * - For spec test, it's true * - For processEpoch flow, it's false, i.e to only update balances once in processRewardsAndPenalties() * - * PERF: Cost 'proportional' to only validators that are slashed. For mainnet conditions: + * PERF: almost no (constant) cost. + * - Total slashings by increment is computed once and stored in state.epochCtx.totalSlashingsByIncrement so no need to compute here + * - Penalties for validators with the same effective balance are the same and computed once + * - No need to apply penalties to validators here, do it once in processRewardsAndPenalties() * - indicesToSlash: max len is 8704. But it's very unlikely since it would require all validators on the same * committees to sign slashable attestations. - * * - On normal mainnet conditions indicesToSlash = 0 + * + * @returns slashing penalties to be applied in processRewardsAndPenalties() */ export function processSlashings( state: CachedBeaconStateAllForks, cache: EpochTransitionCache, updateBalance = true ): number[] { - // No need to compute totalSlashings if there no index to slash + // Return early if there no index to slash if (cache.indicesToSlash.length === 0) { return []; } - // TODO: have the regular totalBalance in EpochTransitionCache too? - const totalBalance = BigInt(cache.totalActiveStakeByIncrement) * BigInt(EFFECTIVE_BALANCE_INCREMENT); - - // TODO: Could totalSlashings be number? - // TODO: Could totalSlashing be cached? - let totalSlashings = BigInt(0); - const slashings = state.slashings.getAll(); - for (let i = 0; i < slashings.length; i++) { - totalSlashings += slashings[i]; - } + const totalBalanceByIncrement = cache.totalActiveStakeByIncrement; const fork = state.config.getForkSeq(state.slot); const proportionalSlashingMultiplier = fork === ForkSeq.phase0 @@ -51,16 +45,20 @@ export function processSlashings( : PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX; const {effectiveBalanceIncrements} = state.epochCtx; - const adjustedTotalSlashingBalance = bigIntMin(totalSlashings * BigInt(proportionalSlashingMultiplier), totalBalance); + const adjustedTotalSlashingBalanceByIncrement = Math.min( + state.epochCtx.totalSlashingsByIncrement * proportionalSlashingMultiplier, + totalBalanceByIncrement + ); const increment = EFFECTIVE_BALANCE_INCREMENT; const penalties: number[] = []; + const penaltiesByEffectiveBalanceIncrement = new Map(); for (const index of cache.indicesToSlash) { const effectiveBalanceIncrement = effectiveBalanceIncrements[index]; let penalty = penaltiesByEffectiveBalanceIncrement.get(effectiveBalanceIncrement); if (penalty === undefined) { - const penaltyNumerator = BigInt(effectiveBalanceIncrement) * adjustedTotalSlashingBalance; - penalty = Number(penaltyNumerator / totalBalance) * increment; + const penaltyNumeratorByIncrement = effectiveBalanceIncrement * adjustedTotalSlashingBalanceByIncrement; + penalty = Math.floor(penaltyNumeratorByIncrement / totalBalanceByIncrement) * increment; penaltiesByEffectiveBalanceIncrement.set(effectiveBalanceIncrement, penalty); } @@ -72,5 +70,21 @@ export function processSlashings( penalties[index] = penalty; } } + return penalties; } + +/** + * Get total slashings by increment. + * By default, total slashings are computed every time we run processSlashings() function above. + * We improve it by computing it once and store it in state.epochCtx.totalSlashingsByIncrement + * Every change to state.slashings should update totalSlashingsByIncrement. + */ +export function getTotalSlashingsByIncrement(state: BeaconStateAllForks): number { + let totalSlashingsByIncrement = 0; + const slashings = state.slashings.getAll(); + for (let i = 0; i < slashings.length; i++) { + totalSlashingsByIncrement += Math.floor(Number(slashings[i]) / EFFECTIVE_BALANCE_INCREMENT); + } + return totalSlashingsByIncrement; +} diff --git a/packages/state-transition/src/epoch/processSlashingsReset.ts b/packages/state-transition/src/epoch/processSlashingsReset.ts index 38fd43ffa6a8..ab4f6fc5571e 100644 --- a/packages/state-transition/src/epoch/processSlashingsReset.ts +++ b/packages/state-transition/src/epoch/processSlashingsReset.ts @@ -1,4 +1,4 @@ -import {EPOCHS_PER_SLASHINGS_VECTOR} from "@lodestar/params"; +import {EFFECTIVE_BALANCE_INCREMENT, EPOCHS_PER_SLASHINGS_VECTOR} from "@lodestar/params"; import {EpochTransitionCache, CachedBeaconStateAllForks} from "../types.js"; /** @@ -10,5 +10,11 @@ export function processSlashingsReset(state: CachedBeaconStateAllForks, cache: E const nextEpoch = cache.currentEpoch + 1; // reset slashings - state.slashings.set(nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR, BigInt(0)); + const slashIndex = nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR; + const oldSlashingValueByIncrement = Math.floor(Number(state.slashings.get(slashIndex)) / EFFECTIVE_BALANCE_INCREMENT); + state.slashings.set(slashIndex, BigInt(0)); + state.epochCtx.totalSlashingsByIncrement = Math.max( + 0, + state.epochCtx.totalSlashingsByIncrement - oldSlashingValueByIncrement + ); } diff --git a/packages/state-transition/test/perf/epoch/epochAltair.test.ts b/packages/state-transition/test/perf/epoch/epochAltair.test.ts index 414cd05164c2..7e84a533e6c6 100644 --- a/packages/state-transition/test/perf/epoch/epochAltair.test.ts +++ b/packages/state-transition/test/perf/epoch/epochAltair.test.ts @@ -126,7 +126,9 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - altair processSlashings`, beforeEach: () => stateOg.value.clone() as CachedBeaconStateAltair, - fn: (state) => processSlashings(state, cache.value), + fn: (state) => { + processSlashings(state, cache.value, false); + }, }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/epochCapella.test.ts b/packages/state-transition/test/perf/epoch/epochCapella.test.ts index 86620fa2dfcf..36d5caf11a99 100644 --- a/packages/state-transition/test/perf/epoch/epochCapella.test.ts +++ b/packages/state-transition/test/perf/epoch/epochCapella.test.ts @@ -105,7 +105,9 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - capella processSlashings`, beforeEach: () => stateOg.value.clone() as CachedBeaconStateCapella, - fn: (state) => processSlashings(state, cache.value), + fn: (state) => { + processSlashings(state, cache.value, false); + }, }); itBench({ diff --git a/packages/state-transition/test/perf/epoch/epochPhase0.test.ts b/packages/state-transition/test/perf/epoch/epochPhase0.test.ts index fa5538a7a66a..ae6b5a536be2 100644 --- a/packages/state-transition/test/perf/epoch/epochPhase0.test.ts +++ b/packages/state-transition/test/perf/epoch/epochPhase0.test.ts @@ -108,7 +108,9 @@ function benchmarkPhase0EpochSteps(stateOg: LazyValue itBench({ id: `${stateId} - phase0 processSlashings`, beforeEach: () => stateOg.value.clone() as CachedBeaconStatePhase0, - fn: (state) => processSlashings(state, cache.value), + fn: (state) => { + processSlashings(state, cache.value, false); + }, }); itBench({ From 3b3d1c00d44dd1e1c1cd7aa4ccdce32daee0fee3 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Nov 2023 10:52:42 +0700 Subject: [PATCH 3/7] feat: store state.slashings as number[] --- packages/state-transition/src/block/slashValidator.ts | 6 ++++-- packages/state-transition/src/epoch/processSlashings.ts | 2 +- .../state-transition/src/epoch/processSlashingsReset.ts | 4 ++-- packages/state-transition/test/utils/capella.ts | 2 +- packages/state-transition/test/utils/state.ts | 4 ++-- packages/types/src/phase0/sszTypes.ts | 3 +-- packages/types/src/primitive/sszTypes.ts | 7 +++++++ 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/state-transition/src/block/slashValidator.ts b/packages/state-transition/src/block/slashValidator.ts index b86272a225b2..e8a5ae977568 100644 --- a/packages/state-transition/src/block/slashValidator.ts +++ b/packages/state-transition/src/block/slashValidator.ts @@ -37,9 +37,11 @@ export function slashValidator( validator.withdrawableEpoch = Math.max(validator.withdrawableEpoch, epoch + EPOCHS_PER_SLASHINGS_VECTOR); const {effectiveBalance} = validator; - // TODO: could state.slashings be number? + // state.slashings is a number because it's reset per epoch in processSlashingsReset() + // for each epoch, there are 8704 max validators to slash so it's safe to use Number + // also we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache const slashingIndex = epoch % EPOCHS_PER_SLASHINGS_VECTOR; - state.slashings.set(slashingIndex, state.slashings.get(slashingIndex) + BigInt(effectiveBalance)); + state.slashings.set(slashingIndex, state.slashings.get(slashingIndex) + effectiveBalance); epochCtx.totalSlashingsByIncrement += effectiveBalanceIncrements[slashedIndex]; const minSlashingPenaltyQuotient = diff --git a/packages/state-transition/src/epoch/processSlashings.ts b/packages/state-transition/src/epoch/processSlashings.ts index 378730fe8ffe..7f4403dc027a 100644 --- a/packages/state-transition/src/epoch/processSlashings.ts +++ b/packages/state-transition/src/epoch/processSlashings.ts @@ -84,7 +84,7 @@ export function getTotalSlashingsByIncrement(state: BeaconStateAllForks): number let totalSlashingsByIncrement = 0; const slashings = state.slashings.getAll(); for (let i = 0; i < slashings.length; i++) { - totalSlashingsByIncrement += Math.floor(Number(slashings[i]) / EFFECTIVE_BALANCE_INCREMENT); + totalSlashingsByIncrement += Math.floor(slashings[i] / EFFECTIVE_BALANCE_INCREMENT); } return totalSlashingsByIncrement; } diff --git a/packages/state-transition/src/epoch/processSlashingsReset.ts b/packages/state-transition/src/epoch/processSlashingsReset.ts index ab4f6fc5571e..6ab22d47526c 100644 --- a/packages/state-transition/src/epoch/processSlashingsReset.ts +++ b/packages/state-transition/src/epoch/processSlashingsReset.ts @@ -11,8 +11,8 @@ export function processSlashingsReset(state: CachedBeaconStateAllForks, cache: E // reset slashings const slashIndex = nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR; - const oldSlashingValueByIncrement = Math.floor(Number(state.slashings.get(slashIndex)) / EFFECTIVE_BALANCE_INCREMENT); - state.slashings.set(slashIndex, BigInt(0)); + const oldSlashingValueByIncrement = Math.floor(state.slashings.get(slashIndex) / EFFECTIVE_BALANCE_INCREMENT); + state.slashings.set(slashIndex, 0); state.epochCtx.totalSlashingsByIncrement = Math.max( 0, state.epochCtx.totalSlashingsByIncrement - oldSlashingValueByIncrement diff --git a/packages/state-transition/test/utils/capella.ts b/packages/state-transition/test/utils/capella.ts index 5789c260f67c..e2cdc47b7e1d 100644 --- a/packages/state-transition/test/utils/capella.ts +++ b/packages/state-transition/test/utils/capella.ts @@ -102,7 +102,7 @@ export function modifyStateSameValidator(seedState: BeaconStateCapella): BeaconS state.eth1DepositIndex = 1000; state.balances.set(0, 30); state.randaoMixes.set(0, crypto.randomBytes(32)); - state.slashings.set(0, 1n); + state.slashings.set(0, 1); state.previousEpochParticipation.set(0, 0b11111110); state.currentEpochParticipation.set(0, 0b11111110); state.justificationBits.set(0, true); diff --git a/packages/state-transition/test/utils/state.ts b/packages/state-transition/test/utils/state.ts index 614210e44afa..29a1f98b5562 100644 --- a/packages/state-transition/test/utils/state.ts +++ b/packages/state-transition/test/utils/state.ts @@ -11,7 +11,7 @@ import {config} from "@lodestar/config/default"; import {createBeaconConfig, ChainForkConfig} from "@lodestar/config"; import {ZERO_HASH} from "../../src/constants/index.js"; -import {newZeroedBigIntArray} from "../../src/util/index.js"; +import {newZeroedArray} from "../../src/util/index.js"; import { BeaconStatePhase0, @@ -64,7 +64,7 @@ export function generateState(opts?: TestBeaconState): BeaconStatePhase0 { validators: [], balances: [], randaoMixes: Array.from({length: EPOCHS_PER_HISTORICAL_VECTOR}, () => ZERO_HASH), - slashings: newZeroedBigIntArray(EPOCHS_PER_SLASHINGS_VECTOR), + slashings: newZeroedArray(EPOCHS_PER_SLASHINGS_VECTOR), previousEpochAttestations: [], currentEpochAttestations: [], justificationBits: ssz.phase0.JustificationBits.defaultValue(), diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index ea06be588849..00dc0101eeea 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -39,7 +39,6 @@ const { EpochInf, CommitteeIndex, ValidatorIndex, - Gwei, Root, Version, ForkDigest, @@ -248,7 +247,7 @@ export const Validator = ValidatorNodeStruct; export const Validators = new ListCompositeType(ValidatorNodeStruct, VALIDATOR_REGISTRY_LIMIT); export const Balances = new ListBasicType(UintNum64, VALIDATOR_REGISTRY_LIMIT); export const RandaoMixes = new VectorCompositeType(Bytes32, EPOCHS_PER_HISTORICAL_VECTOR); -export const Slashings = new VectorBasicType(Gwei, EPOCHS_PER_SLASHINGS_VECTOR); +export const Slashings = new VectorBasicType(primitiveSsz.Slashing, EPOCHS_PER_SLASHINGS_VECTOR); export const JustificationBits = new BitVectorType(JUSTIFICATION_BITS_LENGTH); // Misc dependants diff --git a/packages/types/src/primitive/sszTypes.ts b/packages/types/src/primitive/sszTypes.ts index 65c81d1247b9..5f224a37342c 100644 --- a/packages/types/src/primitive/sszTypes.ts +++ b/packages/types/src/primitive/sszTypes.ts @@ -49,6 +49,13 @@ export const SubcommitteeIndex = UintNum64; */ export const ValidatorIndex = UintNum64; export const WithdrawalIndex = UintNum64; +/** + * Originally this is Gwei but now we switch to a number because: + * - it's reset per epoch in processSlashingsReset() + * - for each epoch, there are 8704 max validators to slash so it's safe to use Number + * - also we don't need to compute the total slashings from `state.slashings`, it's handled by totalSlashingsByIncrement in EpochCache + */ +export const Slashing = UintNum64; export const Gwei = UintBn64; export const Wei = UintBn256; export const Root = new ByteVectorType(32); From c4bf5a70d31fbea2de5401a988f4a395eac6d62b Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 23 Nov 2023 10:56:01 +0700 Subject: [PATCH 4/7] chore: update processSlashingsAllForks.test.ts perf test --- .../test/perf/epoch/processSlashingsAllForks.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/state-transition/test/perf/epoch/processSlashingsAllForks.test.ts b/packages/state-transition/test/perf/epoch/processSlashingsAllForks.test.ts index e3701985dac8..3b0bfa623fb2 100644 --- a/packages/state-transition/test/perf/epoch/processSlashingsAllForks.test.ts +++ b/packages/state-transition/test/perf/epoch/processSlashingsAllForks.test.ts @@ -1,4 +1,5 @@ import {itBench} from "@dapplion/benchmark"; +import {MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; import { beforeProcessEpoch, CachedBeaconStatePhase0, @@ -34,7 +35,9 @@ describe("phase0 processSlashings", () => { minRuns: 5, // Worst case is very slow before: () => getProcessSlashingsTestData(indicesToSlashLen), beforeEach: ({state, cache}) => ({state: state.clone(), cache}), - fn: ({state, cache}) => processSlashings(state as CachedBeaconStatePhase0, cache), + fn: ({state, cache}) => { + processSlashings(state as CachedBeaconStatePhase0, cache, false); + }, }); } }); @@ -48,6 +51,11 @@ function getProcessSlashingsTestData(indicesToSlashLen: number): { } { const state = generatePerfTestCachedStatePhase0({goBackOneSlot: true}); const cache = beforeProcessEpoch(state); + state.slashings.set(0, indicesToSlashLen * MAX_EFFECTIVE_BALANCE); + for (let i = 1; i < state.slashings.length; i++) { + state.slashings.set(i, MAX_EFFECTIVE_BALANCE); + } + state.commit(); cache.indicesToSlash = linspace(indicesToSlashLen); From 0df2f64d0da46a2a08dfcdf9dba21894e281ad1c Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Nov 2023 08:46:10 +0700 Subject: [PATCH 5/7] chore: handle undefined state.slashings[index] just in case --- packages/state-transition/src/block/slashValidator.ts | 2 +- packages/state-transition/src/epoch/index.ts | 2 +- packages/types/src/phase0/sszTypes.ts | 2 +- packages/types/src/primitive/sszTypes.ts | 7 ------- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/state-transition/src/block/slashValidator.ts b/packages/state-transition/src/block/slashValidator.ts index e8a5ae977568..6d1a03db41b8 100644 --- a/packages/state-transition/src/block/slashValidator.ts +++ b/packages/state-transition/src/block/slashValidator.ts @@ -41,7 +41,7 @@ export function slashValidator( // for each epoch, there are 8704 max validators to slash so it's safe to use Number // also we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache const slashingIndex = epoch % EPOCHS_PER_SLASHINGS_VECTOR; - state.slashings.set(slashingIndex, state.slashings.get(slashingIndex) + effectiveBalance); + state.slashings.set(slashingIndex, (state.slashings.get(slashingIndex) ?? 0) + effectiveBalance); epochCtx.totalSlashingsByIncrement += effectiveBalanceIncrements[slashedIndex]; const minSlashingPenaltyQuotient = diff --git a/packages/state-transition/src/epoch/index.ts b/packages/state-transition/src/epoch/index.ts index 51821923cd00..7321d7c75180 100644 --- a/packages/state-transition/src/epoch/index.ts +++ b/packages/state-transition/src/epoch/index.ts @@ -47,7 +47,7 @@ export function processEpoch(fork: ForkSeq, state: CachedBeaconStateAllForks, ca if (fork >= ForkSeq.altair) { processInactivityUpdates(state as CachedBeaconStateAltair, cache); } - // processRewardsAndPenalties() is 2nd step on the specs, we optimize to do it + // processRewardsAndPenalties() is 2nd step in the specs, we optimize to do it // after processSlashings() to update balances only once // processRewardsAndPenalties(state, cache); processRegistryUpdates(state, cache); diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index 00dc0101eeea..72d826a01816 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -247,7 +247,7 @@ export const Validator = ValidatorNodeStruct; export const Validators = new ListCompositeType(ValidatorNodeStruct, VALIDATOR_REGISTRY_LIMIT); export const Balances = new ListBasicType(UintNum64, VALIDATOR_REGISTRY_LIMIT); export const RandaoMixes = new VectorCompositeType(Bytes32, EPOCHS_PER_HISTORICAL_VECTOR); -export const Slashings = new VectorBasicType(primitiveSsz.Slashing, EPOCHS_PER_SLASHINGS_VECTOR); +export const Slashings = new VectorBasicType(UintNum64, EPOCHS_PER_SLASHINGS_VECTOR); export const JustificationBits = new BitVectorType(JUSTIFICATION_BITS_LENGTH); // Misc dependants diff --git a/packages/types/src/primitive/sszTypes.ts b/packages/types/src/primitive/sszTypes.ts index 5f224a37342c..65c81d1247b9 100644 --- a/packages/types/src/primitive/sszTypes.ts +++ b/packages/types/src/primitive/sszTypes.ts @@ -49,13 +49,6 @@ export const SubcommitteeIndex = UintNum64; */ export const ValidatorIndex = UintNum64; export const WithdrawalIndex = UintNum64; -/** - * Originally this is Gwei but now we switch to a number because: - * - it's reset per epoch in processSlashingsReset() - * - for each epoch, there are 8704 max validators to slash so it's safe to use Number - * - also we don't need to compute the total slashings from `state.slashings`, it's handled by totalSlashingsByIncrement in EpochCache - */ -export const Slashing = UintNum64; export const Gwei = UintBn64; export const Wei = UintBn256; export const Root = new ByteVectorType(32); From ac08e7db55b11b2f86f338852b253e1765bdb261 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 24 Nov 2023 10:38:30 +0700 Subject: [PATCH 6/7] chore: give reasoning on how UintNum64 is good for state.slashings --- packages/state-transition/src/block/slashValidator.ts | 9 ++++++--- packages/types/src/phase0/sszTypes.ts | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/state-transition/src/block/slashValidator.ts b/packages/state-transition/src/block/slashValidator.ts index 6d1a03db41b8..ea2ca91e81ee 100644 --- a/packages/state-transition/src/block/slashValidator.ts +++ b/packages/state-transition/src/block/slashValidator.ts @@ -37,9 +37,12 @@ export function slashValidator( validator.withdrawableEpoch = Math.max(validator.withdrawableEpoch, epoch + EPOCHS_PER_SLASHINGS_VECTOR); const {effectiveBalance} = validator; - // state.slashings is a number because it's reset per epoch in processSlashingsReset() - // for each epoch, there are 8704 max validators to slash so it's safe to use Number - // also we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache + + // state.slashings is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: + // - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() + // - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet + // - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + // - we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache const slashingIndex = epoch % EPOCHS_PER_SLASHINGS_VECTOR; state.slashings.set(slashingIndex, (state.slashings.get(slashingIndex) ?? 0) + effectiveBalance); epochCtx.totalSlashingsByIncrement += effectiveBalanceIncrements[slashedIndex]; diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index 72d826a01816..78988465d274 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -247,6 +247,13 @@ export const Validator = ValidatorNodeStruct; export const Validators = new ListCompositeType(ValidatorNodeStruct, VALIDATOR_REGISTRY_LIMIT); export const Balances = new ListBasicType(UintNum64, VALIDATOR_REGISTRY_LIMIT); export const RandaoMixes = new VectorCompositeType(Bytes32, EPOCHS_PER_HISTORICAL_VECTOR); +/** + * This is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: + * - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() + * - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet + * - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + * - we don't need to compute the total slashings from state.slashings, it's handled by totalSlashingsByIncrement in EpochCache + */ export const Slashings = new VectorBasicType(UintNum64, EPOCHS_PER_SLASHINGS_VECTOR); export const JustificationBits = new BitVectorType(JUSTIFICATION_BITS_LENGTH); From bc7cfea4130e2971b77b8d8f08d53d3d55340bb7 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 29 Nov 2023 13:41:29 +0700 Subject: [PATCH 7/7] chore: check network params in processEpoch() --- packages/state-transition/src/epoch/index.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/state-transition/src/epoch/index.ts b/packages/state-transition/src/epoch/index.ts index 7321d7c75180..bb37ed17f4e1 100644 --- a/packages/state-transition/src/epoch/index.ts +++ b/packages/state-transition/src/epoch/index.ts @@ -1,4 +1,10 @@ -import {ForkSeq} from "@lodestar/params"; +import { + ForkSeq, + MAX_ATTESTER_SLASHINGS, + MAX_EFFECTIVE_BALANCE, + MAX_VALIDATORS_PER_COMMITTEE, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; import { CachedBeaconStateAllForks, CachedBeaconStateCapella, @@ -41,8 +47,18 @@ export { }; export {computeUnrealizedCheckpoints} from "./computeUnrealizedCheckpoints.js"; +const maxValidatorsPerStateSlashing = SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE; +const maxSafeValidators = Math.floor(Number.MAX_SAFE_INTEGER / MAX_EFFECTIVE_BALANCE); export function processEpoch(fork: ForkSeq, state: CachedBeaconStateAllForks, cache: EpochTransitionCache): void { + // state.slashings is initially a Gwei (BigInt) vector, however since Nov 2023 it's converted to UintNum64 (number) vector in the state transition because: + // - state.slashings[nextEpoch % EPOCHS_PER_SLASHINGS_VECTOR] is reset per epoch in processSlashingsReset() + // - max slashed validators per epoch is SLOTS_PER_EPOCH * MAX_ATTESTER_SLASHINGS * MAX_VALIDATORS_PER_COMMITTEE which is 32 * 2 * 2048 = 131072 on mainnet + // - with that and 32_000_000_000 MAX_EFFECTIVE_BALANCE, it still fits in a number given that Math.floor(Number.MAX_SAFE_INTEGER / 32_000_000_000) = 281474 + if (maxValidatorsPerStateSlashing > maxSafeValidators) { + throw new Error("Lodestar does not support this network, parameters don't fit number value inside state.slashings"); + } + processJustificationAndFinalization(state, cache); if (fork >= ForkSeq.altair) { processInactivityUpdates(state as CachedBeaconStateAltair, cache);