Skip to content

Commit

Permalink
Merge bc7cfea into 248b64c
Browse files Browse the repository at this point in the history
  • Loading branch information
twoeths committed Nov 29, 2023
2 parents 248b64c + bc7cfea commit 67f801c
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 40 deletions.
12 changes: 9 additions & 3 deletions packages/state-transition/src/block/slashValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -37,9 +37,15 @@ 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 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) + BigInt(effectiveBalance));
state.slashings.set(slashingIndex, (state.slashings.get(slashingIndex) ?? 0) + effectiveBalance);
epochCtx.totalSlashingsByIncrement += effectiveBalanceIncrements[slashedIndex];

const minSlashingPenaltyQuotient =
fork === ForkSeq.phase0
Expand Down
10 changes: 10 additions & 0 deletions packages/state-transition/src/cache/epochCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
/**
Expand Down Expand Up @@ -206,6 +211,7 @@ export class EpochCache {
currentShuffling: EpochShuffling;
nextShuffling: EpochShuffling;
effectiveBalanceIncrements: EffectiveBalanceIncrements;
totalSlashingsByIncrement: number;
syncParticipantReward: number;
syncProposerReward: number;
baseRewardPerIncrement: number;
Expand All @@ -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;
Expand Down Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -425,6 +433,7 @@ export class EpochCache {
currentShuffling,
nextShuffling,
effectiveBalanceIncrements,
totalSlashingsByIncrement,
syncParticipantReward,
syncProposerReward,
baseRewardPerIncrement,
Expand Down Expand Up @@ -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,
Expand Down
26 changes: 23 additions & 3 deletions packages/state-transition/src/epoch/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -41,15 +47,29 @@ 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);
}
processRewardsAndPenalties(state, cache);
// 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);
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
77 changes: 56 additions & 21 deletions packages/state-transition/src/epoch/processSlashings.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {bigIntMin} from "@lodestar/utils";
import {
EFFECTIVE_BALANCE_INCREMENT,
ForkSeq,
Expand All @@ -8,33 +7,35 @@ 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
* 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:
* 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): void {
// No need to compute totalSlashings if there no index to slash
export function processSlashings(
state: CachedBeaconStateAllForks,
cache: EpochTransitionCache,
updateBalance = true
): number[] {
// 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];
return [];
}

const totalBalanceByIncrement = cache.totalActiveStakeByIncrement;
const fork = state.config.getForkSeq(state.slot);
const proportionalSlashingMultiplier =
fork === ForkSeq.phase0
Expand All @@ -44,12 +45,46 @@ export function processSlashings(state: CachedBeaconStateAllForks, cache: EpochT
: 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<number, number>();
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 penaltyNumeratorByIncrement = effectiveBalanceIncrement * adjustedTotalSlashingBalanceByIncrement;
penalty = Math.floor(penaltyNumeratorByIncrement / totalBalanceByIncrement) * 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;
}

/**
* 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(slashings[i] / EFFECTIVE_BALANCE_INCREMENT);
}
return totalSlashingsByIncrement;
}
10 changes: 8 additions & 2 deletions packages/state-transition/src/epoch/processSlashingsReset.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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(state.slashings.get(slashIndex) / EFFECTIVE_BALANCE_INCREMENT);
state.slashings.set(slashIndex, 0);
state.epochCtx.totalSlashingsByIncrement = Math.max(
0,
state.epochCtx.totalSlashingsByIncrement - oldSlashingValueByIncrement
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue<CachedBeaconStateAllForks>
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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue<CachedBeaconStateAllForks>
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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ function benchmarkPhase0EpochSteps(stateOg: LazyValue<CachedBeaconStateAllForks>
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({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {itBench} from "@dapplion/benchmark";
import {MAX_EFFECTIVE_BALANCE} from "@lodestar/params";
import {
beforeProcessEpoch,
CachedBeaconStatePhase0,
Expand Down Expand Up @@ -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);
},
});
}
});
Expand All @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion packages/state-transition/test/utils/capella.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions packages/state-transition/test/utils/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
10 changes: 8 additions & 2 deletions packages/types/src/phase0/sszTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ const {
EpochInf,
CommitteeIndex,
ValidatorIndex,
Gwei,
Root,
Version,
ForkDigest,
Expand Down Expand Up @@ -248,7 +247,14 @@ 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);
/**
* 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);

// Misc dependants
Expand Down

0 comments on commit 67f801c

Please sign in to comment.