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

Add sync committee assertions #4582

Merged
merged 10 commits into from
Sep 23, 2022
5 changes: 5 additions & 0 deletions packages/cli/test/simulation/simulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
attestationPerSlotAssertions,
finalityAssertions,
headsAssertions,
syncCommitteeAssertions,
} from "../utils/simulation/assertions.js";

chai.use(chaiAsPromised);
Expand Down Expand Up @@ -128,6 +129,10 @@ for (const {beaconNodes, validatorClients, validatorsPerClient} of nodeCases) {
describe("attestation participation", () => {
attestationParticipationAssertions(env, epoch);
});

describe("sync committee participation", () => {
syncCommitteeAssertions(env, epoch);
});
});
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class SimulationEnvironment {
readonly clock: EpochClock;
readonly acceptableParticipationRate = 1;
readonly acceptableMaxInclusionDelay = 1;
readonly acceptableMinSyncParticipation = 1;
readonly tracker: SimulationTracker;
readonly emitter: EventEmitter;
readonly controller: AbortController;
Expand Down
31 changes: 30 additions & 1 deletion packages/cli/test/utils/simulation/SimulationTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import {altair, Epoch, Slot} from "@lodestar/types";
import {toHexString} from "@lodestar/utils";
import {EpochClock} from "./EpochClock.js";
import {BeaconNodeProcess, SimulationParams} from "./types.js";
import {computeAttestation, computeAttestationParticipation, computeInclusionDelay, getForkName} from "./utils.js";
import {
avg,
computeAttestation,
computeAttestationParticipation,
computeInclusionDelay,
computeSyncCommitteeParticipation,
getForkName,
} from "./utils.js";

const participationHeading = (id: string): string => `${id}-P-H/S/T`;
const missedBlocksHeading = (id: string): string => `${id}-M`;
// const nodeHeadHeading = (id: string): string => `${id}-H`;
const finalizedHeading = (id: string): string => `${id}-F`;
const syncCommitteeHeading = (id: string): string => `${id}-SC`;

export class SimulationTracker {
readonly producedBlocks: Map<string, Map<Slot, boolean>>;
Expand All @@ -19,6 +27,7 @@ export class SimulationTracker {
private lastSeenSlot: Map<string, Slot>;
readonly headPerSlot: Map<string, Map<Slot, string>>;
readonly finalizedPerSlot: Map<string, Map<Slot, Slot>>;
readonly syncCommitteeParticipation: Map<string, Map<Slot, number>>;

readonly emitter = new EventEmitter();

Expand All @@ -40,6 +49,7 @@ export class SimulationTracker {
this.lastSeenSlot = new Map();
this.headPerSlot = new Map();
this.finalizedPerSlot = new Map();
this.syncCommitteeParticipation = new Map();

for (let i = 0; i < nodes.length; i += 1) {
this.producedBlocks.set(nodes[i].id, new Map());
Expand All @@ -49,9 +59,11 @@ export class SimulationTracker {
this.lastSeenSlot.set(nodes[i].id, 0);
this.headPerSlot.set(nodes[i].id, new Map());
this.finalizedPerSlot.set(nodes[i].id, new Map());
this.syncCommitteeParticipation.set(nodes[i].id, new Map());

// Set finalized slot to genesis
this.finalizedPerSlot.get(nodes[i].id)?.set(0, 0);
this.syncCommitteeParticipation.get(nodes[i].id)?.set(0, params.altairEpoch === 0 ? 1 : 0);
}
}

Expand Down Expand Up @@ -107,6 +119,7 @@ export class SimulationTracker {
const slot = event.slot;
const lastSeenSlot = this.lastSeenSlot.get(node.id);
const blockAttestations = await node.api.beacon.getBlockAttestations(slot);
const block = await node.api.beacon.getBlockV2(slot);

if (lastSeenSlot !== undefined && slot > lastSeenSlot) {
this.lastSeenSlot.set(node.id, slot);
Expand All @@ -115,6 +128,9 @@ export class SimulationTracker {
this.producedBlocks.get(node.id)?.set(slot, true);
this.attestationsPerSlot.get(node.id)?.set(slot, computeAttestation(blockAttestations.data));
this.inclusionDelayPerBlock.get(node.id)?.set(slot, computeInclusionDelay(blockAttestations.data, slot));
this.syncCommitteeParticipation
.get(node.id)
?.set(slot, computeSyncCommitteeParticipation(block.version, block.data as altair.SignedBeaconBlock));

const head = await node.api.beacon.getBlockHeader("head");
this.headPerSlot.get(node.id)?.set(slot, toHexString(head.data.root));
Expand Down Expand Up @@ -181,10 +197,17 @@ export class SimulationTracker {
}`;
}

for (const node of this.nodes) {
record[syncCommitteeHeading(node.id)] = this.syncCommitteeParticipation.get(node.id)?.get(slot)?.toFixed(2);
}

records.push(record);

if (this.clock.isLastSlotOfEpoch(slot)) {
const epoch = this.clock.getEpochForSlot(slot);
const firstSlot = this.clock.getFirstSlotOfEpoch(epoch);
const lastSlot = this.clock.getLastSlotOfEpoch(epoch);

const record: Record<string, unknown> = {
F: getForkName(epoch, this.params),
Eph: epoch,
Expand All @@ -204,6 +227,12 @@ export class SimulationTracker {
)}`
: "";
record[participationHeading(node.id)] = participationStr;

const syncParticipation: number[] = [];
for (let i = firstSlot; i <= lastSlot; i++) {
syncParticipation.push(this.syncCommitteeParticipation.get(node.id)?.get(i) ?? 0);
}
record[syncCommitteeHeading(node.id)] = avg(syncParticipation).toFixed(2);
}
records.push(record);
}
Expand Down
31 changes: 31 additions & 0 deletions packages/cli/test/utils/simulation/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,34 @@ export function headsAssertions(env: SimulationEnvironment, epoch: Epoch): void
});
}
}

export function syncCommitteeAssertions(env: SimulationEnvironment, epoch: Epoch): void {
if (epoch < env.params.altairEpoch) {
return;
}

for (const node of env.nodes) {
describe(node.id, () => {
const startSlot = env.clock.getFirstSlotOfEpoch(epoch);
const endSlot = env.clock.getLastSlotOfEpoch(epoch);
const altairStartSlot = env.clock.getFirstSlotOfEpoch(env.params.altairEpoch);

for (let slot = startSlot; slot <= endSlot; slot++) {
// Sync committee is not available before until 2 slots for altair epoch
if (slot === altairStartSlot || slot === altairStartSlot + 1) {
continue;
}

it(`should have have higher participation for slot "${slot}"`, () => {
const participation = env.tracker.syncCommitteeParticipation.get(env.nodes[0].id)?.get(slot);
const acceptableMinSyncParticipation = env.acceptableMinSyncParticipation;

expect(participation).to.gte(
acceptableMinSyncParticipation,
`node "${node.id}" low sync committee participation slot: ${slot}, participation: ${participation}, acceptableMinSyncParticipation: ${acceptableMinSyncParticipation}`
);
});
}
});
}
}
9 changes: 9 additions & 0 deletions packages/cli/test/utils/simulation/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ export const computeInclusionDelay = (attestations: phase0.Attestation[], slot:
return avg(Array.from(attestations).map((att) => slot - att.data.slot));
};

export const computeSyncCommitteeParticipation = (version: ForkName, block: altair.SignedBeaconBlock): number => {
if (version === ForkName.phase0) {
return 0;
}

const {syncCommitteeBits} = block.message.body.syncAggregate;
return syncCommitteeBits.getTrueBitIndexes().length / syncCommitteeBits.bitLen;
};

export const avg = (arr: number[]): number => {
return arr.length === 0 ? 0 : arr.reduce((p, c) => p + c, 0) / arr.length;
};
Expand Down