Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve prepareNextEpoch #7171

Draft
wants to merge 15 commits into
base: unstable
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/persistent-merkle-tree": "^0.8.0",
"@chainsafe/ssz": "^0.17.1",
"@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree",
"@chainsafe/ssz": "file:../../../ssz/packages/ssz",
"@lodestar/config": "^1.22.0",
"@lodestar/params": "^1.22.0",
"@lodestar/types": "^1.22.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,15 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/as-sha256": "^0.5.0",
"@chainsafe/as-sha256": "file:../../../ssz/packages/as-sha256",
"@chainsafe/blst": "^2.0.3",
"@chainsafe/discv5": "^10.0.1",
"@chainsafe/enr": "^4.0.1",
"@chainsafe/libp2p-gossipsub": "^14.1.0",
"@chainsafe/libp2p-noise": "^16.0.0",
"@chainsafe/persistent-merkle-tree": "^0.8.0",
"@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree",
"@chainsafe/prometheus-gc-stats": "^1.0.0",
"@chainsafe/ssz": "^0.17.1",
"@chainsafe/ssz": "file:../../../ssz/packages/ssz",
"@chainsafe/threads": "^1.11.1",
"@chainsafe/pubkey-index-map": "2.0.0",
"@ethersproject/abi": "^5.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import {BlockProcessOpts} from "../options.js";
import {byteArrayEquals} from "../../util/bytes.js";
import {nextEventLoop} from "../../util/eventLoop.js";
import {BlockInput, ImportBlockOpts} from "./types.js";
import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree";

/**
* Data in a BeaconBlock is bounded so we can use a single HashComputationGroup for all blocks
*/
const blockHCGroup = new HashComputationGroup();

/**
* Verifies 1 or more blocks are fully valid running the full state transition; from a linear sequence of blocks.
Expand Down Expand Up @@ -63,7 +69,8 @@ export async function verifyBlocksStateTransitionOnly(
const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({
source: StateHashTreeRootSource.blockTransition,
});
const stateRoot = postState.hashTreeRoot();
// state root is computed inside stateTransition(), so it should take no time here
const stateRoot = postState.batchHashTreeRoot(blockHCGroup);
hashTreeRootTimer?.();

// Check state root matches
Expand Down
13 changes: 12 additions & 1 deletion packages/beacon-node/src/chain/prepareNextSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ import {prepareExecutionPayload, getPayloadAttributesForSSE} from "./produceBloc
import {IBeaconChain} from "./interface.js";
import {RegenCaller} from "./regen/index.js";
import {ForkchoiceCaller} from "./forkChoice/index.js";
import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree";

/* With 12s slot times, this scheduler will run 4s before the start of each slot (`12 / 3 = 4`). */
export const SCHEDULER_LOOKAHEAD_FACTOR = 3;

/* We don't want to do more epoch transition than this */
const PREPARE_EPOCH_LIMIT = 1;

/**
* The same HashComputationGroup to be used for all epoch transition.
*/
const epochHCGroup = new HashComputationGroup();

/**
* At Bellatrix, if we are responsible for proposing in next slot, we want to prepare payload
* 4s (1/3 slot) before the start of next slot
Expand Down Expand Up @@ -232,7 +238,12 @@ export class PrepareNextSlotScheduler {
const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({
source: isEpochTransition ? StateHashTreeRootSource.prepareNextEpoch : StateHashTreeRootSource.prepareNextSlot,
});
state.hashTreeRoot();
if (isEpochTransition) {
state.batchHashTreeRoot(epochHCGroup);
} else {
// normal slot, not worth to batch hash
state.node.rootHashObject;
}
hashTreeRootTimer?.();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import {
import {BeaconBlock, BlindedBeaconBlock, Gwei, Root} from "@lodestar/types";
import {ZERO_HASH} from "../../constants/index.js";
import {Metrics} from "../../metrics/index.js";
import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree";

/**
* Data in a BeaconBlock is bounded so we can use a single HashComputationGroup for all blocks
*/
const blockHCGroup = new HashComputationGroup();

/**
* Instead of running fastStateTransition(), only need to process block since
Expand Down Expand Up @@ -48,7 +54,8 @@ export function computeNewStateRoot(
const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({
source: StateHashTreeRootSource.computeNewStateRoot,
});
const newStateRoot = postState.hashTreeRoot();
// state root is computed inside stateTransition(), so it should take no time here
const newStateRoot = postState.batchHashTreeRoot(blockHCGroup);
hashTreeRootTimer?.();

return {newStateRoot, proposerReward};
Expand Down
9 changes: 3 additions & 6 deletions packages/beacon-node/test/spec/utils/runValidSszTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,11 @@ export function runValidSszTest(type: Type<unknown>, testData: ValidTestCaseData
// 0x0000000000000000000000000000000000000000000000000000000000000000
if (process.env.RENDER_ROOTS) {
if (type.isBasic) {
console.log("ROOTS Basic", toHexString(type.serialize(testDataValue)));
console.log("Chunk Basic", toHexString(type.serialize(testDataValue)));
} else {
// biome-ignore lint/complexity/useLiteralKeys: The `getRoots` is a protected attribute
const roots = (type as CompositeType<unknown, unknown, unknown>)["getRoots"](testDataValue);
console.log(
"ROOTS Composite",
roots.map((root) => toHexString(root))
);
const chunkBytes = (type as CompositeType<unknown, unknown, unknown>)["getChunkBytes"](testDataValue);
console.log("Chunk Bytes Composite", toHexString(chunkBytes));
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
"@chainsafe/blst": "^2.0.3",
"@chainsafe/discv5": "^10.0.1",
"@chainsafe/enr": "^4.0.1",
"@chainsafe/persistent-merkle-tree": "^0.8.0",
"@chainsafe/ssz": "^0.17.1",
"@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree",
"@chainsafe/ssz": "file:../../../ssz/packages/ssz",
"@chainsafe/threads": "^1.11.1",
"@libp2p/crypto": "^5.0.4",
"@libp2p/interface": "^2.1.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/applyPreset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// MUST import this file first before anything and not import any Lodestar code.

import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/as-sha256.js";
import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/hashtree.js";
import {setHasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/index.js";

// without setting this first, persistent-merkle-tree will use noble instead
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"blockchain"
],
"dependencies": {
"@chainsafe/ssz": "^0.17.1",
"@chainsafe/ssz": "file:../../../ssz/packages/ssz",
"@lodestar/params": "^1.22.0",
"@lodestar/utils": "^1.22.0",
"@lodestar/types": "^1.22.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/ssz": "^0.17.1",
"@chainsafe/ssz": "file:../../../ssz/packages/ssz",
"@lodestar/config": "^1.22.0",
"@lodestar/utils": "^1.22.0",
"classic-level": "^1.4.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/fork-choice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/ssz": "^0.17.1",
"@chainsafe/ssz": "file:../../../ssz/packages/ssz",
"@lodestar/config": "^1.22.0",
"@lodestar/params": "^1.22.0",
"@lodestar/state-transition": "^1.22.0",
Expand Down
6 changes: 3 additions & 3 deletions packages/light-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@
"dependencies": {
"@chainsafe/bls": "7.1.3",
"@chainsafe/blst": "^0.2.0",
"@chainsafe/persistent-merkle-tree": "^0.8.0",
"@chainsafe/ssz": "^0.17.1",
"@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree",
"@chainsafe/ssz": "file:../../../ssz/packages/ssz",
"@lodestar/api": "^1.22.0",
"@lodestar/config": "^1.22.0",
"@lodestar/params": "^1.22.0",
Expand All @@ -85,7 +85,7 @@
"mitt": "^3.0.0"
},
"devDependencies": {
"@chainsafe/as-sha256": "^0.5.0",
"@chainsafe/as-sha256": "file:../../../ssz/packages/as-sha256",
"@types/qs": "^6.9.7",
"fastify": "^5.0.0",
"qs": "^6.11.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/prover/src/cli/applyPreset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// MUST import this file first before anything and not import any Lodestar code.

import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/as-sha256.js";
import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/hashtree.js";
import {setHasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/index.js";

// without setting this first, persistent-merkle-tree will use noble instead
Expand Down
6 changes: 3 additions & 3 deletions packages/state-transition/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@
},
"types": "lib/index.d.ts",
"dependencies": {
"@chainsafe/as-sha256": "^0.5.0",
"@chainsafe/as-sha256": "file:../../../ssz/packages/as-sha256",
"@chainsafe/blst": "^2.0.3",
"@chainsafe/persistent-merkle-tree": "^0.8.0",
"@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree",
"@chainsafe/persistent-ts": "^0.19.1",
"@chainsafe/ssz": "^0.17.1",
"@chainsafe/ssz": "file:../../../ssz/packages/ssz",
"@chainsafe/swap-or-not-shuffle": "^0.0.2",
"@lodestar/config": "^1.22.0",
"@lodestar/params": "^1.22.0",
Expand Down
8 changes: 4 additions & 4 deletions packages/state-transition/src/block/processEth1Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ export function becomesNewEth1Data(
// Then isEqualEth1DataView compares cached roots (HashObject as of Jan 2022) which is much cheaper
// than doing structural equality, which requires tree -> value conversions
let sameVotesCount = 0;
const eth1DataVotes = state.eth1DataVotes.getAllReadonly();
for (let i = 0; i < eth1DataVotes.length; i++) {
if (isEqualEth1DataView(eth1DataVotes[i], newEth1Data)) {
// biome-ignore lint/complexity/noForEach: ssz api
state.eth1DataVotes.forEach((eth1DataVote) => {
if (isEqualEth1DataView(eth1DataVote, newEth1Data)) {
sameVotesCount++;
}
}
});

// The +1 is to account for the `eth1Data` supplied to the function.
if ((sameVotesCount + 1) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD) {
Expand Down
59 changes: 35 additions & 24 deletions packages/state-transition/src/cache/epochTransitionCache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {phase0, Epoch, RootHex, ValidatorIndex} from "@lodestar/types";
import {Epoch, RootHex, ValidatorIndex} from "@lodestar/types";
import {intDiv, toRootHex} from "@lodestar/utils";
import {
EPOCHS_PER_SLASHINGS_VECTOR,
Expand All @@ -19,7 +19,12 @@ import {
FLAG_CURR_TARGET_ATTESTER,
FLAG_CURR_HEAD_ATTESTER,
} from "../util/attesterStatus.js";
import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../index.js";
import {
CachedBeaconStateAllForks,
CachedBeaconStateAltair,
CachedBeaconStatePhase0,
hasCompoundingWithdrawalCredential,
} from "../index.js";
import {computeBaseRewardPerIncrement} from "../util/altair.js";
import {processPendingAttestations} from "../epoch/processPendingAttestations.js";

Expand Down Expand Up @@ -133,11 +138,7 @@ export interface EpochTransitionCache {

flags: number[];

/**
* Validators in the current epoch, should use it for read-only value instead of accessing state.validators directly.
* Note that during epoch processing, validators could be updated so need to use it with care.
*/
validators: phase0.Validator[];
isCompoundingValidatorArr: boolean[];

/**
* This is for electra only
Expand Down Expand Up @@ -216,6 +217,11 @@ const inclusionDelays = new Array<number>();
const flags = new Array<number>();
/** WARNING: reused, never gc'd */
const nextEpochShufflingActiveValidatorIndices = new Array<number>();
/** WARNING: reused, never gc'd */
const isCompoundingValidatorArr = new Array<boolean>();

const previousEpochParticipation = new Array<number>();
const currentEpochParticipation = new Array<number>();

export function beforeProcessEpoch(
state: CachedBeaconStateAllForks,
Expand All @@ -233,17 +239,14 @@ export function beforeProcessEpoch(

const indicesToSlash: ValidatorIndex[] = [];
const indicesEligibleForActivationQueue: ValidatorIndex[] = [];
const indicesEligibleForActivation: ValidatorIndex[] = [];
const indicesEligibleForActivation: {validatorIndex: ValidatorIndex; activationEligibilityEpoch: Epoch}[] = [];
const indicesToEject: ValidatorIndex[] = [];

let totalActiveStakeByIncrement = 0;

// To optimize memory each validator node in `state.validators` is represented with a special node type
// `BranchNodeStruct` that represents the data as struct internally. This utility grabs the struct data directly
// from the nodes without any extra transformation. The returned `validators` array contains native JS objects.
const validators = state.validators.getAllReadonlyValues();
const validatorCount = validators.length;

const validatorCount = state.validators.length;
if (forkSeq >= ForkSeq.electra) {
isCompoundingValidatorArr.length = validatorCount;
}
nextEpochShufflingActiveValidatorIndices.length = validatorCount;
let nextEpochShufflingActiveIndicesLength = 0;
// pre-fill with true (most validators are active)
Expand Down Expand Up @@ -273,10 +276,13 @@ export function beforeProcessEpoch(

const effectiveBalancesByIncrements = epochCtx.effectiveBalanceIncrements;

for (let i = 0; i < validatorCount; i++) {
const validator = validators[i];
state.validators.forEachValue((validator, i) => {
let flag = 0;

if (forkSeq >= ForkSeq.electra) {
isCompoundingValidatorArr[i] = hasCompoundingWithdrawalCredential(validator.withdrawalCredentials);
}

if (validator.slashed) {
if (slashingsEpoch === validator.withdrawableEpoch) {
indicesToSlash.push(i);
Expand Down Expand Up @@ -339,7 +345,10 @@ export function beforeProcessEpoch(
//
// Use `else` since indicesEligibleForActivationQueue + indicesEligibleForActivation are mutually exclusive
else if (validator.activationEpoch === FAR_FUTURE_EPOCH && validator.activationEligibilityEpoch <= currentEpoch) {
indicesEligibleForActivation.push(i);
indicesEligibleForActivation.push({
validatorIndex: i,
activationEligibilityEpoch: validator.activationEligibilityEpoch,
});
}

// To optimize process_registry_updates():
Expand All @@ -364,7 +373,7 @@ export function beforeProcessEpoch(
if (isActiveNext2) {
nextEpochShufflingActiveValidatorIndices[nextEpochShufflingActiveIndicesLength++] = i;
}
}
});

// Trigger async build of shuffling for epoch after next (nextShuffling post epoch transition)
const epochAfterNext = state.epochCtx.nextEpoch + 1;
Expand Down Expand Up @@ -396,7 +405,7 @@ export function beforeProcessEpoch(
// To optimize process_registry_updates():
// order by sequence of activationEligibilityEpoch setting and then index
indicesEligibleForActivation.sort(
(a, b) => validators[a].activationEligibilityEpoch - validators[b].activationEligibilityEpoch || a - b
(a, b) => a.activationEligibilityEpoch - b.activationEligibilityEpoch || a.validatorIndex - b.validatorIndex
);

if (forkSeq === ForkSeq.phase0) {
Expand Down Expand Up @@ -427,8 +436,10 @@ export function beforeProcessEpoch(
FLAG_CURR_HEAD_ATTESTER
);
} else {
const previousEpochParticipation = (state as CachedBeaconStateAltair).previousEpochParticipation.getAll();
const currentEpochParticipation = (state as CachedBeaconStateAltair).currentEpochParticipation.getAll();
previousEpochParticipation.length = (state as CachedBeaconStateAltair).previousEpochParticipation.length;
(state as CachedBeaconStateAltair).previousEpochParticipation.getAll(previousEpochParticipation);
currentEpochParticipation.length = (state as CachedBeaconStateAltair).currentEpochParticipation.length;
(state as CachedBeaconStateAltair).currentEpochParticipation.getAll(currentEpochParticipation);
for (let i = 0; i < validatorCount; i++) {
flags[i] |=
// checking active status first is required to pass random spec tests in altair
Expand Down Expand Up @@ -505,7 +516,7 @@ export function beforeProcessEpoch(
currEpochUnslashedTargetStakeByIncrement: currTargetUnslStake,
indicesToSlash,
indicesEligibleForActivationQueue,
indicesEligibleForActivation,
indicesEligibleForActivation: indicesEligibleForActivation.map(({validatorIndex}) => validatorIndex),
indicesToEject,
nextShufflingDecisionRoot,
nextShufflingActiveIndices,
Expand All @@ -517,7 +528,7 @@ export function beforeProcessEpoch(
proposerIndices,
inclusionDelays,
flags,
validators,
isCompoundingValidatorArr,
// will be assigned in processPendingConsolidations()
newCompoundingValidators: undefined,
// Will be assigned in processRewardsAndPenalties()
Expand Down
Loading