Skip to content

Commit

Permalink
test: add fork-choice benchmark for updateHead (#5577)
Browse files Browse the repository at this point in the history
* Add fork-choice benchmark for updateHead

* Add more benchmarks
  • Loading branch information
dapplion authored May 31, 2023
1 parent cef8562 commit b7bedf1
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 183 deletions.
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

0 comments on commit b7bedf1

Please sign in to comment.