Skip to content

Commit

Permalink
fix: improve forkchoice updateHead() time (#5867)
Browse files Browse the repository at this point in the history
* chore: update computeDeltas perf test

* perf: avoid too many short lived objects in computeDeltas

* chore: more tests for different vaildator number

* chore: add protoNodes param to computeDeltas test

* chore: update updateHead perf test with 1 day of unfinalized blocks

* fix: improve nodeIsViableForHead for ProtoArray

* chore: create cached indices inside computeDeltas()

* chore: revise comments
  • Loading branch information
twoeths authored Aug 11, 2023
1 parent 22aa2f0 commit 7b38a1a
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 93 deletions.
33 changes: 26 additions & 7 deletions packages/fork-choice/src/protoArray/computeDeltas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,54 @@ export function computeDeltas(
newBalances: EffectiveBalanceIncrements,
equivocatingIndices: Set<ValidatorIndex>
): number[] {
const deltas = Array.from({length: indices.size}, () => 0);
const deltas = Array<number>(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<string, number>();

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
// votes are for the zero hash (genesis block)
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({
Expand All @@ -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({
Expand All @@ -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({
Expand Down
33 changes: 30 additions & 3 deletions packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
4 changes: 2 additions & 2 deletions packages/fork-choice/test/perf/forkChoice/updateHead.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]) {
Expand Down
137 changes: 56 additions & 81 deletions packages/fork-choice/test/perf/protoArray/computeDeltas.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, number> = new Map<string, number>();
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<string, number> = new Map<string, number>();
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});
},
});
}
});

0 comments on commit 7b38a1a

Please sign in to comment.