Skip to content
Closed
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
34 changes: 29 additions & 5 deletions packages/state-transition/src/cache/epochCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,8 @@ export class EpochCache {
/** TODO: Indexed SyncCommitteeCache */
nextSyncCommitteeIndexed: SyncCommitteeCache;

// TODO GLOAS: See if we need to cached PTC for prev/next epoch
// PTC for current epoch, computed eagerly at epoch transition
// PTC for previous and current epochs, computed eagerly at epoch transition
previousPayloadTimelinessCommittees: Uint32Array[];
payloadTimelinessCommittees: Uint32Array[];

// TODO: Helper stats
Expand Down Expand Up @@ -275,6 +275,7 @@ export class EpochCache {
previousTargetUnslashedBalanceIncrements: number;
currentSyncCommitteeIndexed: SyncCommitteeCache;
nextSyncCommitteeIndexed: SyncCommitteeCache;
previousPayloadTimelinessCommittees: Uint32Array[];
payloadTimelinessCommittees: Uint32Array[];
epoch: Epoch;
syncPeriod: SyncPeriod;
Expand Down Expand Up @@ -306,6 +307,7 @@ export class EpochCache {
this.previousTargetUnslashedBalanceIncrements = data.previousTargetUnslashedBalanceIncrements;
this.currentSyncCommitteeIndexed = data.currentSyncCommitteeIndexed;
this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed;
this.previousPayloadTimelinessCommittees = data.previousPayloadTimelinessCommittees;
this.payloadTimelinessCommittees = data.payloadTimelinessCommittees;
this.epoch = data.epoch;
this.syncPeriod = data.syncPeriod;
Expand Down Expand Up @@ -458,6 +460,9 @@ export class EpochCache {
}

// Compute PTC eagerly for all slots in the epoch
// Includes previous-epoch committees for epoch-boundary lookups (slot N block validating slot N-1 attestation)
// after restart / fresh cache initialization.
let previousPayloadTimelinessCommittees: Uint32Array[] = [];
let payloadTimelinessCommittees: Uint32Array[] = [];
if (currentEpoch >= config.GLOAS_FORK_EPOCH) {
payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
Expand All @@ -466,6 +471,15 @@ export class EpochCache {
currentShuffling.committees,
effectiveBalanceIncrements
);

if (previousEpoch >= config.GLOAS_FORK_EPOCH) {
previousPayloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
state,
previousEpoch,
previousShuffling.committees,
effectiveBalanceIncrements
);
}
}

// Precompute churnLimit for efficient initiateValidatorExit() during block proposing MUST be recompute everytime the
Expand Down Expand Up @@ -541,6 +555,7 @@ export class EpochCache {
currentTargetUnslashedBalanceIncrements,
currentSyncCommitteeIndexed,
nextSyncCommitteeIndexed,
previousPayloadTimelinessCommittees,
payloadTimelinessCommittees,
epoch: currentEpoch,
syncPeriod: computeSyncPeriodAtEpoch(currentEpoch),
Expand Down Expand Up @@ -587,6 +602,7 @@ export class EpochCache {
currentTargetUnslashedBalanceIncrements: this.currentTargetUnslashedBalanceIncrements,
currentSyncCommitteeIndexed: this.currentSyncCommitteeIndexed,
nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed,
previousPayloadTimelinessCommittees: this.previousPayloadTimelinessCommittees,
payloadTimelinessCommittees: this.payloadTimelinessCommittees,
epoch: this.epoch,
syncPeriod: this.syncPeriod,
Expand Down Expand Up @@ -698,6 +714,7 @@ export class EpochCache {

this.proposersPrevEpoch = this.proposers;
if (upcomingEpoch >= this.config.GLOAS_FORK_EPOCH) {
this.previousPayloadTimelinessCommittees = this.payloadTimelinessCommittees;
this.payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
state,
upcomingEpoch,
Expand Down Expand Up @@ -1029,11 +1046,18 @@ export class EpochCache {
throw new Error("Payload Timeliness Committee is not available before gloas fork");
}

if (epoch !== this.epoch) {
throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`);
if (epoch === this.epoch) {
return this.payloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
}

if (epoch === this.epoch - 1) {
const committee = this.previousPayloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
if (committee !== undefined) {
return committee;
}
}

return this.payloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`);
}

getIndexedPayloadAttestation(
Expand Down
92 changes: 92 additions & 0 deletions packages/state-transition/test/unit/cache/epochCache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {describe, expect, it} from "vitest";
import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map";
import {SLOTS_PER_EPOCH} from "@lodestar/params";
import {RootHex} from "@lodestar/types";
import {EpochCache} from "../../../src/cache/epochCache.js";
import {SyncCommitteeCacheEmpty} from "../../../src/cache/syncCommitteeCache.js";
import {EpochShuffling} from "../../../src/util/epochShuffling.js";

const ZERO_ROOT_HEX = ("0x" + "00".repeat(32)) as RootHex;

function createShuffling(epoch: number): EpochShuffling {
return {
epoch,
activeIndices: new Uint32Array([0]),
shuffling: new Uint32Array([0]),
committees: Array.from({length: SLOTS_PER_EPOCH}, () => [new Uint32Array([0])]),
committeesPerSlot: 1,
};
}

function createEpochCacheForPayloadCommitteeTest(): EpochCache {
const config = {GLOAS_FORK_EPOCH: 1, ELECTRA_FORK_EPOCH: 0} as any;

const previousPayloadTimelinessCommittees = Array.from(
{length: SLOTS_PER_EPOCH},
(_, i) => new Uint32Array([100 + i])
);
const payloadTimelinessCommittees = Array.from({length: SLOTS_PER_EPOCH}, (_, i) => new Uint32Array([200 + i]));

return new EpochCache({
config,
pubkey2index: new PubkeyIndexMap(),
index2pubkey: [],
proposers: [0],
proposersPrevEpoch: [0],
proposersNextEpoch: {computed: true, indexes: [0]},
previousDecisionRoot: ZERO_ROOT_HEX,
currentDecisionRoot: ZERO_ROOT_HEX,
nextDecisionRoot: ZERO_ROOT_HEX,
previousShuffling: createShuffling(5),
currentShuffling: createShuffling(6),
nextShuffling: createShuffling(7),
nextActiveIndices: new Uint32Array([0]),
effectiveBalanceIncrements: new Uint16Array([32]),
totalSlashingsByIncrement: 0,
syncParticipantReward: 0,
syncProposerReward: 0,
baseRewardPerIncrement: 0,
totalActiveBalanceIncrements: 1,
churnLimit: 1,
activationChurnLimit: 1,
exitQueueEpoch: 0,
exitQueueChurn: 0,
currentTargetUnslashedBalanceIncrements: 0,
previousTargetUnslashedBalanceIncrements: 0,
currentSyncCommitteeIndexed: new SyncCommitteeCacheEmpty(),
nextSyncCommitteeIndexed: new SyncCommitteeCacheEmpty(),
previousPayloadTimelinessCommittees,
payloadTimelinessCommittees,
epoch: 6,
syncPeriod: 0,
});
}

describe("EpochCache.getPayloadTimelinessCommittee", () => {
it("returns PTC for previous epoch slot (epoch boundary previous slot)", () => {
const epochCtx = createEpochCacheForPayloadCommitteeTest();
const previousSlot = epochCtx.epoch * SLOTS_PER_EPOCH - 1;

expect(epochCtx.getPayloadTimelinessCommittee(previousSlot)).toEqual(
epochCtx.previousPayloadTimelinessCommittees[previousSlot % SLOTS_PER_EPOCH]
);
});

it("returns PTC for current epoch slot", () => {
const epochCtx = createEpochCacheForPayloadCommitteeTest();
const currentSlot = epochCtx.epoch * SLOTS_PER_EPOCH;

expect(epochCtx.getPayloadTimelinessCommittee(currentSlot)).toEqual(
epochCtx.payloadTimelinessCommittees[currentSlot % SLOTS_PER_EPOCH]
);
});

it("throws for slots older than previous epoch", () => {
const epochCtx = createEpochCacheForPayloadCommitteeTest();
const tooOldSlot = (epochCtx.epoch - 2) * SLOTS_PER_EPOCH;

expect(() => epochCtx.getPayloadTimelinessCommittee(tooOldSlot)).toThrow(
`Payload Timeliness Committee is not available for slot=${tooOldSlot}`
);
});
});
Loading