Skip to content

Commit

Permalink
feat: forkchoice to track head vote by proto index
Browse files Browse the repository at this point in the history
  • Loading branch information
twoeths committed Aug 14, 2023
1 parent 5edda2b commit 67099c2
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 141 deletions.
29 changes: 23 additions & 6 deletions packages/fork-choice/src/forkChoice/forkChoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export class ForkChoice implements IForkChoice {
const oldBalances = this.balances;
const newBalances = this.fcStore.justified.balances;
const deltas = computeDeltas(
this.protoArray.indices,
this.protoArray.indices.size,
this.votes,
oldBalances,
newBalances,
Expand Down Expand Up @@ -555,7 +555,7 @@ export class ForkChoice implements IForkChoice {
}
return {
epoch: vote.nextEpoch,
root: vote.nextRoot,
root: this.protoArray.nodes[vote.nextIndex].blockRoot,
};
}

Expand Down Expand Up @@ -665,8 +665,19 @@ export class ForkChoice implements IForkChoice {
return this.protoArray.isDescendant(ancestorRoot, descendantRoot);
}

/**
* All indices in votes are relative to proto array so always keep it up to date
*/
prune(finalizedRoot: RootHex): ProtoBlock[] {
return this.protoArray.maybePrune(finalizedRoot);
const prunedNodes = this.protoArray.maybePrune(finalizedRoot);
const prunedCount = prunedNodes.length;
for (const vote of this.votes) {
if (vote.currentIndex !== null) {
vote.currentIndex -= prunedCount;
}
vote.nextIndex -= prunedCount;
}
return prunedNodes;
}

setPruneThreshold(threshold: number): void {
Expand Down Expand Up @@ -1091,14 +1102,20 @@ export class ForkChoice implements IForkChoice {
*/
private addLatestMessage(validatorIndex: ValidatorIndex, nextEpoch: Epoch, nextRoot: RootHex): void {
const vote = this.votes[validatorIndex];
// should not happen, attestation is validated before this step
const nextIndex = this.protoArray.indices.get(nextRoot);
if (nextIndex === undefined) {
throw new Error(`Could not find proto index for nextRoot ${nextRoot}`);
}

if (vote === undefined) {
this.votes[validatorIndex] = {
currentRoot: HEX_ZERO_HASH,
nextRoot,
currentIndex: null,
nextIndex,
nextEpoch,
};
} else if (nextEpoch > vote.nextEpoch) {
vote.nextRoot = nextRoot;
vote.nextIndex = nextIndex;
vote.nextEpoch = nextEpoch;
}
// else its an old vote, don't count it
Expand Down
78 changes: 27 additions & 51 deletions packages/fork-choice/src/protoArray/computeDeltas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ValidatorIndex} from "@lodestar/types";
import {EffectiveBalanceIncrements} from "@lodestar/state-transition";
import {VoteTracker, HEX_ZERO_HASH} from "./interface.js";
import {VoteTracker} from "./interface.js";
import {ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";

/**
Expand All @@ -13,31 +13,16 @@ import {ProtoArrayError, ProtoArrayErrorCode} from "./errors.js";
* - If a value in `indices` is greater to or equal to `indices.length`.
*/
export function computeDeltas(
indices: Map<string, number>,
numProtoNodes: number,
votes: VoteTracker[],
oldBalances: EffectiveBalanceIncrements,
newBalances: EffectiveBalanceIncrements,
equivocatingIndices: Set<ValidatorIndex>
): number[] {
const deltas = Array<number>(indices.size).fill(0);
const zeroHash = HEX_ZERO_HASH;
const deltas = Array<number>(numProtoNodes).fill(0);
// 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;
};
let currentIndex, nextIndex: number;

for (let vIndex = 0; vIndex < votes.length; vIndex++) {
const vote = votes[vIndex];
Expand All @@ -46,11 +31,8 @@ export function computeDeltas(
if (vote === undefined) {
continue;
}
currentRoot = vote.currentRoot;
nextRoot = vote.nextRoot;
if (currentRoot === zeroHash && nextRoot === zeroHash) {
continue;
}
currentIndex = vote.currentIndex;
nextIndex = vote.nextIndex;

// IF the validator was not included in the _old_ balances (i.e. it did not exist yet)
// then say its balance was 0
Expand All @@ -65,49 +47,43 @@ export function computeDeltas(

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) {
currentDeltaIndex = getIndex(currentRoot);
if (currentDeltaIndex !== undefined) {
if (currentDeltaIndex >= deltas.length) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
index: currentDeltaIndex,
});
}
deltas[currentDeltaIndex] -= oldBalance;
if (currentIndex !== null) {
if (currentIndex >= numProtoNodes) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
index: currentIndex,
});
}
deltas[currentIndex] -= oldBalance;
}
vote.currentRoot = zeroHash;
vote.currentIndex = null;
continue;
}

if (currentRoot !== nextRoot || oldBalance !== newBalance) {
if (currentIndex !== nextIndex || 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
currentDeltaIndex = getIndex(currentRoot);
if (currentDeltaIndex !== undefined) {
if (currentDeltaIndex >= deltas.length) {
if (currentIndex !== null) {
if (currentIndex >= numProtoNodes) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
index: currentDeltaIndex,
index: currentIndex,
});
}
deltas[currentDeltaIndex] -= oldBalance;
deltas[currentIndex] -= oldBalance;
}

// 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
nextDeltaIndex = getIndex(nextRoot);
if (nextDeltaIndex !== undefined) {
if (nextDeltaIndex >= deltas.length) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
index: nextDeltaIndex,
});
}
deltas[nextDeltaIndex] += newBalance;
if (nextIndex >= numProtoNodes) {
throw new ProtoArrayError({
code: ProtoArrayErrorCode.INVALID_NODE_DELTA,
index: nextIndex,
});
}
deltas[nextIndex] += newBalance;
}
vote.currentRoot = nextRoot;
vote.currentIndex = nextIndex;
}

return deltas;
Expand Down
6 changes: 4 additions & 2 deletions packages/fork-choice/src/protoArray/interface.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import {Epoch, Slot, RootHex, UintNum64} from "@lodestar/types";

// TODO: remove
// RootHex is a root as a hex string
// Used for lightweight and easy comparison
export const HEX_ZERO_HASH = "0x0000000000000000000000000000000000000000000000000000000000000000";

/**
* Simplified 'latest message' with previous message
* The index is relative to ProtoArray indices
*/
export type VoteTracker = {
currentRoot: RootHex;
nextRoot: RootHex;
currentIndex: number | null;
nextIndex: number;
nextEpoch: Epoch;
};

Expand Down
16 changes: 3 additions & 13 deletions packages/fork-choice/test/perf/protoArray/computeDeltas.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import crypto from "node:crypto";
import {itBench, setBenchOpts} from "@dapplion/benchmark";
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";
Expand All @@ -10,8 +8,6 @@ describe("computeDeltas", () => {
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;
Expand All @@ -35,28 +31,22 @@ describe("computeDeltas", () => {
});

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);
}
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,
currentIndex: Math.floor(numProtoNode / 2),
nextIndex: Math.floor(numProtoNode / 2) + 1,
nextEpoch: epoch,
});
}
return votes;
},
fn: (votes) => {
computeDeltas(indices, votes, oldBalances, newBalances, new Set());
computeDeltas(numProtoNode, votes, oldBalances, newBalances, new Set());
},
});
}
Expand Down
Loading

0 comments on commit 67099c2

Please sign in to comment.