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

test: add fork-choice benchmark for updateHead #5577

Merged
merged 2 commits into from
May 31, 2023
Merged
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
6 changes: 3 additions & 3 deletions packages/fork-choice/src/protoArray/computeDeltas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ export function computeDeltas(

// 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;
const 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;
const newBalance = newBalances[vIndex] ?? 0;

if (equivocatingIndices.has(vIndex)) {
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);
Expand Down
17 changes: 6 additions & 11 deletions packages/fork-choice/src/protoArray/protoArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,24 +216,19 @@ export class ProtoArray {
bestDescendant: undefined,
};

let nodeIndex = this.nodes.length;
const nodeIndex = this.nodes.length;

this.indices.set(node.blockRoot, nodeIndex);
this.nodes.push(node);

let parentIndex = node.parent;
// If this node is valid, lets propagate the valid status up the chain
// and throw error if we counter invalid, as this breaks consensus
if (node.executionStatus === ExecutionStatus.Valid && parentIndex !== undefined) {
this.propagateValidExecutionStatusByIndex(parentIndex);
}
if (node.parent !== undefined) {
this.maybeUpdateBestChildAndDescendant(node.parent, nodeIndex, currentSlot);

let n: ProtoNode | undefined = node;
while (parentIndex !== undefined) {
this.maybeUpdateBestChildAndDescendant(parentIndex, nodeIndex, currentSlot);
nodeIndex = parentIndex;
n = this.getNodeByIndex(nodeIndex);
parentIndex = n?.parent;
if (node.executionStatus === ExecutionStatus.Valid) {
this.propagateValidExecutionStatusByIndex(node.parent);
}
}
}

Expand Down
169 changes: 0 additions & 169 deletions packages/fork-choice/test/perf/forkChoice/forkChoice.test.ts

This file was deleted.

94 changes: 94 additions & 0 deletions packages/fork-choice/test/perf/forkChoice/onAttestation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {itBench} from "@dapplion/benchmark";
import {AttestationData, IndexedAttestation} from "@lodestar/types/phase0";
import {ATTESTATION_SUBNET_COUNT} from "@lodestar/params";
import {ssz} from "@lodestar/types";
import {computeEpochAtSlot} from "@lodestar/state-transition";
import {fromHexString, toHexString} from "@chainsafe/ssz";
import {initializeForkChoice} from "./util.js";

describe("ForkChoice onAttestation", () => {
/**
* Committee: | ----------- 0 --------------| ... | ----------------------- i --------------------- | ------------------------63 -------------------------|
* Validator index: | 0 1 2 ... committeeLength-1 | ... | (i*committeeLengh + ) 0 1 2 ... committeeLengh-1| (63*committeeLengh +) 0 1 2 ... committeeLength - 1 |
*/
itBench({
id: "pass gossip attestations to forkchoice per slot",
beforeEach: () => {
const initialBlockCount = 64;
const forkchoice = initializeForkChoice({
initialBlockCount,
initialValidatorCount: 600_000,
initialEquivocatedCount: 0,
});
const head = forkchoice.updateHead();

// at slot 64, forkchoice receives attestations of slot 63
forkchoice.updateTime(initialBlockCount);
// there are 700 aggregate and proof max per slot
// as of Jan 2022
const committeeLength = 135;
// considering TARGET_AGGREGATORS_PER_COMMITTEE=16, it's not likely we have more than this number of aggregators
// connect to node per slot
const numAggregatorsConnectedToNode = 3;
const attestationDataOmitIndex: Omit<AttestationData, "index"> = {
beaconBlockRoot: fromHexString(head.blockRoot),
slot: initialBlockCount - 1,
source: {
epoch: head.justifiedEpoch,
root: fromHexString(head.justifiedRoot),
},
target: {
epoch: computeEpochAtSlot(head.slot),
root: fromHexString(head.targetRoot),
},
};

// unaggregatedAttestations: aggregator {i} for committee index {i}
const unaggregatedAttestations: IndexedAttestation[] = [];
for (let committeeIndex = 0; committeeIndex < numAggregatorsConnectedToNode; committeeIndex++) {
const attestationData: AttestationData = {
...attestationDataOmitIndex,
index: committeeIndex,
};
for (let i = 0; i < committeeLength; i++) {
const validatorIndex = committeeIndex * committeeLength + i;
unaggregatedAttestations.push({
attestingIndices: [validatorIndex],
data: attestationData,
signature: Buffer.alloc(96),
});
}
}

// aggregated attestations: each committee index has 11 aggregators in average
// 64 committee indices map to 704 aggregated attestations per slot
const aggregatedAttestations: IndexedAttestation[] = [];
const averageAggregatorsPerSlot = 11;
for (let committeeIndex = 0; committeeIndex < ATTESTATION_SUBNET_COUNT; committeeIndex++) {
const tbAttestationData = {
...attestationDataOmitIndex,
index: committeeIndex,
};

// cache the root
ssz.phase0.AttestationData.hashTreeRoot(tbAttestationData);

for (let aggregator = 0; aggregator < averageAggregatorsPerSlot; aggregator++) {
// same data, different signatures
aggregatedAttestations.push({
attestingIndices: Array.from({length: committeeLength}, (_, i) => committeeIndex * committeeLength + i),
data: tbAttestationData,
signature: Buffer.alloc(96, aggregator),
});
}
}

return {forkchoice, allAttestationsPerSlot: [...unaggregatedAttestations, ...aggregatedAttestations]};
},
fn: ({forkchoice, allAttestationsPerSlot}) => {
for (const attestation of allAttestationsPerSlot) {
forkchoice.onAttestation(attestation, toHexString(ssz.phase0.AttestationData.hashTreeRoot(attestation.data)));
}
},
});
});
66 changes: 66 additions & 0 deletions packages/fork-choice/test/perf/forkChoice/updateHead.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {itBench} from "@dapplion/benchmark";
import {computeEpochAtSlot} from "@lodestar/state-transition";
import {ForkChoice, ProtoBlock} from "../../../src/index.js";
import {initializeForkChoice, Opts} from "./util.js";

describe("forkchoice updateHead", () => {
for (const initialValidatorCount of [100_000, 600_000, 1_000_000]) {
runUpdateHeadBenchmark({initialValidatorCount, initialBlockCount: 64, initialEquivocatedCount: 0});
}

for (const initialBlockCount of [
// 10 epochs of blocks ~ 1 hour
10 * 32,
// 4 hours of blocks
(4 * 60 * 60) / 12,
// // 1 day of blocks
// (24 * 60 * 60) / 12,
// // 20 days of blocks
// (20 * 24 * 60 * 60) / 12,
]) {
runUpdateHeadBenchmark({initialValidatorCount: 600_000, initialBlockCount, initialEquivocatedCount: 0});
}

for (const initialEquivocatedCount of [1_000, 10_000, 300_000]) {
runUpdateHeadBenchmark({initialValidatorCount: 600_000, initialBlockCount: 64, initialEquivocatedCount});
}

function runUpdateHeadBenchmark(opts: Opts): void {
itBench({
id: `forkChoice updateHead vc ${opts.initialValidatorCount} bc ${opts.initialBlockCount} eq ${opts.initialEquivocatedCount}`,
before: () => {
const forkChoice = initializeForkChoice(opts);

const vote1 = forkChoice.updateHead();
const vote2 = forkChoice.getBlockHex(vote1.parentRoot);
if (!vote2) throw Error("no vote2");
if (vote1.blockRoot === vote2.blockRoot) throw Error("blockRoot vote1 == vote2");
if (vote1.slot === vote2.slot) throw Error("slot vote1 == vote2");

return {
forkChoice,
vote1,
vote2,
currentVoteIs1: true,
};
},
beforeEach: (data) => {
// Flip all votes every run
everyoneVotes(data.currentVoteIs1 ? data.vote2 : data.vote1, data.forkChoice);
data.currentVoteIs1 = !data.currentVoteIs1;
return data;
},
fn: ({forkChoice}) => {
forkChoice.updateHead();
},
});
}
});

function everyoneVotes(vote: ProtoBlock, forkChoice: ForkChoice): void {
const nextEpoch = computeEpochAtSlot(vote.slot);
const nextRoot = vote.blockRoot;
for (let i = 0; i < forkChoice["balances"].length; i++) {
forkChoice["addLatestMessage"](i, nextEpoch, nextRoot);
}
}
Loading