diff --git a/packages/fork-choice/src/protoArray/computeDeltas.ts b/packages/fork-choice/src/protoArray/computeDeltas.ts index 6b2d1133d37c..b90219b680ef 100644 --- a/packages/fork-choice/src/protoArray/computeDeltas.ts +++ b/packages/fork-choice/src/protoArray/computeDeltas.ts @@ -19,8 +19,26 @@ export function computeDeltas( newBalances: EffectiveBalanceIncrements, equivocatingIndices: Set ): number[] { - const deltas = Array.from({length: indices.size}, () => 0); + const deltas = Array(indices.size).fill(0); const zeroHash = HEX_ZERO_HASH; + // avoid creating new variables in the loop to potentially reduce GC pressure + let oldBalance, newBalance: number; + let currentRoot, nextRoot: string; + let currentDeltaIndex, nextDeltaIndex: number | undefined; + // this function tends to get some very few roots from `indices` so create a small cache to improve performance + const cachedIndices = new Map(); + + const getIndex = (root: string): number | undefined => { + let index = cachedIndices.get(root); + if (index === undefined) { + index = indices.get(root); + if (index !== undefined) { + cachedIndices.set(root, index); + } + } + return index; + }; + for (let vIndex = 0; vIndex < votes.length; vIndex++) { const vote = votes[vIndex]; // There is no need to create a score change if the validator has never voted or both of their @@ -28,26 +46,27 @@ export function computeDeltas( if (vote === undefined) { continue; } - const {currentRoot, nextRoot} = vote; + currentRoot = vote.currentRoot; + nextRoot = vote.nextRoot; if (currentRoot === zeroHash && nextRoot === zeroHash) { continue; } // IF the validator was not included in the _old_ balances (i.e. it did not exist yet) // then say its balance was 0 - const oldBalance = oldBalances[vIndex] ?? 0; + oldBalance = oldBalances[vIndex] ?? 0; // If the validator's vote is not known in the _new_ balances, then use a balance of zero. // // It is possible that there was a vote for an unknown validator if we change our justified // state to a new state with a higher epoch that is on a different fork because that fork may have // on-boarded fewer validators than the prior fork. - const newBalance = newBalances[vIndex] ?? 0; + newBalance = newBalances[vIndex] ?? 0; if (equivocatingIndices.size > 0 && equivocatingIndices.has(vIndex)) { // this function could be called multiple times but we only want to process slashing validator for 1 time if (currentRoot !== zeroHash) { - const currentDeltaIndex = indices.get(currentRoot); + currentDeltaIndex = getIndex(currentRoot); if (currentDeltaIndex !== undefined) { if (currentDeltaIndex >= deltas.length) { throw new ProtoArrayError({ @@ -65,7 +84,7 @@ export function computeDeltas( if (currentRoot !== nextRoot || oldBalance !== newBalance) { // We ignore the vote if it is not known in `indices . // We assume that it is outside of our tree (ie: pre-finalization) and therefore not interesting - const currentDeltaIndex = indices.get(currentRoot); + currentDeltaIndex = getIndex(currentRoot); if (currentDeltaIndex !== undefined) { if (currentDeltaIndex >= deltas.length) { throw new ProtoArrayError({ @@ -77,7 +96,7 @@ export function computeDeltas( } // We ignore the vote if it is not known in `indices . // We assume that it is outside of our tree (ie: pre-finalization) and therefore not interesting - const nextDeltaIndex = indices.get(nextRoot); + nextDeltaIndex = getIndex(nextRoot); if (nextDeltaIndex !== undefined) { if (nextDeltaIndex >= deltas.length) { throw new ProtoArrayError({ diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index 513d37c393b0..a09980b56aff 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -740,12 +740,39 @@ export class ProtoArray { correctJustified = node.unrealizedJustifiedEpoch >= previousEpoch && votingSourceEpoch + 2 >= currentEpoch; } - const finalizedSlot = computeStartSlotAtEpoch(this.finalizedEpoch); - const correctFinalized = - this.finalizedRoot === this.getAncestorOrNull(node.blockRoot, finalizedSlot) || this.finalizedEpoch === 0; + const correctFinalized = this.finalizedEpoch === 0 || this.isFinalizedRootOrDescendant(node); return correctJustified && correctFinalized; } + /** + * Return `true` if `node` is equal to or a descendant of the finalized node. + * This function helps improve performance of nodeIsViableForHead a lot by avoiding + * the loop inside `getAncestors`. + */ + isFinalizedRootOrDescendant(node: ProtoNode): boolean { + // The finalized and justified checkpoints represent a list of known + // ancestors of `node` that are likely to coincide with the store's + // finalized checkpoint. + if (node.finalizedEpoch === this.finalizedEpoch && node.finalizedRoot === this.finalizedRoot) { + return true; + } + + if (node.justifiedEpoch === this.finalizedEpoch && node.justifiedRoot === this.finalizedRoot) { + return true; + } + + if (node.unrealizedFinalizedEpoch === this.finalizedEpoch && node.unrealizedFinalizedRoot === this.finalizedRoot) { + return true; + } + + if (node.unrealizedJustifiedEpoch === this.finalizedEpoch && node.unrealizedJustifiedRoot === this.finalizedRoot) { + return true; + } + + const finalizedSlot = computeStartSlotAtEpoch(this.finalizedEpoch); + return this.finalizedEpoch === 0 || this.finalizedRoot === this.getAncestorOrNull(node.blockRoot, finalizedSlot); + } + /** * Same to getAncestor but it may return null instead of throwing error */ diff --git a/packages/fork-choice/test/perf/forkChoice/updateHead.test.ts b/packages/fork-choice/test/perf/forkChoice/updateHead.test.ts index 0a2289d1f6a5..850215f5a844 100644 --- a/packages/fork-choice/test/perf/forkChoice/updateHead.test.ts +++ b/packages/fork-choice/test/perf/forkChoice/updateHead.test.ts @@ -13,8 +13,8 @@ describe("forkchoice updateHead", () => { 10 * 32, // 4 hours of blocks (4 * 60 * 60) / 12, - // // 1 day of blocks - // (24 * 60 * 60) / 12, + // 1 day of blocks + (24 * 60 * 60) / 12, // // 20 days of blocks // (20 * 24 * 60 * 60) / 12, ]) { diff --git a/packages/fork-choice/test/perf/protoArray/computeDeltas.test.ts b/packages/fork-choice/test/perf/protoArray/computeDeltas.test.ts index f8f1fef38e15..44df7e481b9f 100644 --- a/packages/fork-choice/test/perf/protoArray/computeDeltas.test.ts +++ b/packages/fork-choice/test/perf/protoArray/computeDeltas.test.ts @@ -1,98 +1,73 @@ +import crypto from "node:crypto"; import {itBench, setBenchOpts} from "@dapplion/benchmark"; -import {expect} from "chai"; -import { - CachedBeaconStateAltair, - computeStartSlotAtEpoch, - EffectiveBalanceIncrements, - getEffectiveBalanceIncrementsZeroed, -} from "@lodestar/state-transition"; -import {TIMELY_SOURCE_FLAG_INDEX} from "@lodestar/params"; -// eslint-disable-next-line import/no-relative-packages -import {generatePerfTestCachedStateAltair} from "../../../../state-transition/test/perf/util.js"; +import {toHexString} from "@chainsafe/ssz"; +import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsZeroed} from "@lodestar/state-transition"; import {VoteTracker} from "../../../src/protoArray/interface.js"; import {computeDeltas} from "../../../src/protoArray/computeDeltas.js"; import {computeProposerBoostScoreFromBalances} from "../../../src/forkChoice/forkChoice.js"; -/** Same to https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.5/specs/altair/beacon-chain.md#has_flag */ -const TIMELY_SOURCE = 1 << TIMELY_SOURCE_FLAG_INDEX; -function flagIsTimelySource(flag: number): boolean { - return (flag & TIMELY_SOURCE) === TIMELY_SOURCE; -} - describe("computeDeltas", () => { - let originalState: CachedBeaconStateAltair; - const indices: Map = new Map(); let oldBalances: EffectiveBalanceIncrements; let newBalances: EffectiveBalanceIncrements; const oldRoot = "0x32dec344944029ba183ac387a7aa1f2068591c00e9bfadcfb238e50fbe9ea38e"; const newRoot = "0xb59f3a209f639dd6b5645ea9fad8d441df44c3be93bd1bbf50ef90bf124d1238"; + const oneHourProtoNodes = (60 * 60) / 12; + const fourHourProtoNodes = 4 * oneHourProtoNodes; + const oneDayProtoNodes = 24 * oneHourProtoNodes; + // 2 first numbers are respective to number of validators in goerli, mainnet as of Aug 2023 + const numValidators = [500_000, 750_000, 1_400_000, 2_100_000]; + for (const numValidator of numValidators) { + before(function () { + this.timeout(2 * 60 * 1000); + oldBalances = getEffectiveBalanceIncrementsZeroed(numValidator); + newBalances = getEffectiveBalanceIncrementsZeroed(numValidator); - before(function () { - this.timeout(2 * 60 * 1000); // Generating the states for the first time is very slow - - originalState = generatePerfTestCachedStateAltair({goBackOneSlot: true}); - - const previousEpochParticipationArr = originalState.previousEpochParticipation.getAll(); - const currentEpochParticipationArr = originalState.currentEpochParticipation.getAll(); - - const numPreviousEpochParticipation = previousEpochParticipationArr.filter(flagIsTimelySource).length; - const numCurrentEpochParticipation = currentEpochParticipationArr.filter(flagIsTimelySource).length; - - expect(numPreviousEpochParticipation).to.equal(250000, "Wrong numPreviousEpochParticipation"); - expect(numCurrentEpochParticipation).to.equal(250000, "Wrong numCurrentEpochParticipation"); - - oldBalances = getEffectiveBalanceIncrementsZeroed(numPreviousEpochParticipation); - newBalances = getEffectiveBalanceIncrementsZeroed(numPreviousEpochParticipation); - - for (let i = 0; i < numPreviousEpochParticipation; i++) { - oldBalances[i] = 32; - newBalances[i] = 32; - } - for (let i = 0; i < 10000; i++) { - indices.set("" + i, i); - } - indices.set(oldRoot, 1001); - indices.set(newRoot, 1001); - }); + for (let i = 0; i < numValidator; i++) { + oldBalances[i] = 32; + newBalances[i] = 32; + } + }); - setBenchOpts({ - minMs: 30 * 1000, - maxMs: 40 * 1000, - }); + setBenchOpts({ + minMs: 30 * 1000, + maxMs: 40 * 1000, + }); - itBench({ - id: "computeDeltas", - beforeEach: () => { - const votes: VoteTracker[] = []; - const epoch = originalState.epochCtx.currentShuffling.epoch; - const committee = originalState.epochCtx.getBeaconCommittee(computeStartSlotAtEpoch(epoch), 0); - for (let i = 0; i < 250000; i++) { - if (committee.includes(i)) { - votes.push({ - currentRoot: oldRoot, - nextRoot: newRoot, - nextEpoch: epoch, - }); - } else { - votes.push({ - currentRoot: oldRoot, - nextRoot: oldRoot, - nextEpoch: epoch - 1, - }); - } + for (const numProtoNode of [oneHourProtoNodes, fourHourProtoNodes, oneDayProtoNodes]) { + const indices: Map = new Map(); + for (let i = 0; i < numProtoNode; i++) { + indices.set(toHexString(crypto.randomBytes(32)), i); } - return votes; - }, - fn: (votes) => { - computeDeltas(indices, votes, oldBalances, newBalances, new Set()); - }, - }); + indices.set(oldRoot, Math.floor(numProtoNode / 2)); + indices.set(newRoot, Math.floor(numProtoNode / 2) + 1); + itBench({ + id: `computeDeltas ${numValidator} validators ${numProtoNode} proto nodes`, + beforeEach: () => { + const votes: VoteTracker[] = []; + const epoch = 100_000; + for (let i = 0; i < numValidator; i++) { + votes.push({ + currentRoot: oldRoot, + nextRoot: newRoot, + nextEpoch: epoch, + }); + } + return votes; + }, + fn: (votes) => { + computeDeltas(indices, votes, oldBalances, newBalances, new Set()); + }, + }); + } + } - itBench({ - id: "computeProposerBoostScoreFromBalances", - fn: () => { - computeProposerBoostScoreFromBalances(newBalances, {slotsPerEpoch: 32, proposerScoreBoost: 70}); - }, - }); + for (const numValidator of numValidators) { + itBench({ + id: `computeProposerBoostScoreFromBalances ${numValidator} validators`, + fn: () => { + computeProposerBoostScoreFromBalances(newBalances, {slotsPerEpoch: 32, proposerScoreBoost: 70}); + }, + }); + } });