Skip to content

Commit a87a98f

Browse files
matthewkeilwemeetagaintwoeths
authored andcommitted
feat: async shuffling refactor (#6938)
* feat: add ShufflingCache to EpochCache * fix: implementation in state-transition for EpochCache with ShufflingCache * feat: remove shufflingCache.processState * feat: implement ShufflingCache changes in beacon-node * feat: pass shufflingCache when loading cached state from db * test: fix state-transition tests for EpochCache changes * feat: Pass shufflingCache to EpochCache at startup * test: fix slot off by one for decision root in perf test * chore: use ?. syntax * chore: refactoring * feat: add comments and clean up afterProcessEpoch * fix: perf test slot incrementing * fix: remove MockShufflingCache * Revert "chore: refactoring" This reverts commit 104aa56. * refactor: shufflingCache getters * refactor: shufflingCache setters * refactor: build and getOrBuild * docs: add comments to ShufflingCache methods * chore: lint issues * test: update tests in beacon-node * chore: lint * feat: get shufflings from cache for API * feat: minTimeDelayToBuildShuffling cli flag * test: fix shufflingCache promise insertion test * fix: rebase conflicts * fix: changes from debugging sim tests * refactor: minimize changes in afterProcessEpoch * chore: fix lint * chore: fix check-types * chore: fix check-types * feat: add diff utility * fix: bug in spec tests from invalid nextActiveIndices * refactor: add/remove comments * refactor: remove this.activeIndicesLength from EpochCache * refactor: simplify shufflingCache.getSync * refactor: remove unnecessary undefined's * refactor: clean up ShufflingCache unit test * feat: add metrics for ShufflingCache * feat: add shufflingCache metrics to state-transition * chore: lint * fix: metric name clash * refactor: add comment about not having ShufflingCache in EpochCache * refactor: rename shuffling decision root functions * refactor: remove unused comment * feat: async add nextShuffling to EpochCache after its built * feat: make ShufflingCache.set private * feat: chance metrics to nextShufflingNotOnEpochCache instead of positive case * refactor: move diff to separate PR * chore: fix tests using shufflingCache.set method * feat: remove minTimeDelayToBuild * feat: return promise from insertPromise and then through build * fix: update metrics names and help field * feat: move build of shuffling to beforeProcessEpoch * feat: allow calc of pivot slot before slot increment * fix: calc of pivot slot before slot increment * Revert "fix: calc of pivot slot before slot increment" This reverts commit 5e65f7e. * Revert "feat: allow calc of pivot slot before slot increment" This reverts commit ed850ee. * feat: allow getting current block root for shuffling calculation * fix: get nextShufflingDecisionRoot directly from state.blockRoots * fix: convert toRootHex * docs: add comment about pulling decisionRoot directly from state * feat: add back metrics for regen attestation cache hit/miss * docs: fix docstring on shufflingCache.build * refactor: change validatorIndices to Uint32Array * refactor: remove comment and change variable name * fix: use toRootHex instead of toHexString * refactor: deduplicate moved function computeAnchorCheckpoint * fix: touch up metrics per PR comments * fix: merge conflict * chore: lint * refactor: add scope around activeIndices to GC arrays * feat: directly use Uint32Array instead of transcribing number array to Uint32Array * refactor: activeIndices per tuyen comment * refactor: rename to epochAfterNext * chore: review PR * feat: update no shuffling ApiError to 500 status * fix: add back unnecessary eslint directive. to be remove under separate PR * feat: update no shuffling ApiError to 500 status * docs: add comment about upcomingEpoch --------- Co-authored-by: Cayman <caymannava@gmail.com> Co-authored-by: Tuyen Nguyen <vutuyen2636@gmail.com>
1 parent 7722762 commit a87a98f

34 files changed

+700
-362
lines changed

packages/beacon-node/src/api/impl/beacon/state/index.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,14 @@ export function getBeaconStateApi({
202202

203203
const epoch = filters.epoch ?? computeEpochAtSlot(state.slot);
204204
const startSlot = computeStartSlotAtEpoch(epoch);
205-
const shuffling = stateCached.epochCtx.getShufflingAtEpoch(epoch);
205+
const decisionRoot = stateCached.epochCtx.getShufflingDecisionRoot(epoch);
206+
const shuffling = await chain.shufflingCache.get(epoch, decisionRoot);
207+
if (!shuffling) {
208+
throw new ApiError(
209+
500,
210+
`No shuffling found to calculate committees for epoch: ${epoch} and decisionRoot: ${decisionRoot}`
211+
);
212+
}
206213
const committees = shuffling.committees;
207214
const committeesFlat = committees.flatMap((slotCommittees, slotInEpoch) => {
208215
const slot = startSlot + slotInEpoch;

packages/beacon-node/src/api/impl/validator/index.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {ApplicationMethods} from "@lodestar/api/server";
33
import {
44
CachedBeaconStateAllForks,
55
computeStartSlotAtEpoch,
6+
calculateCommitteeAssignments,
67
proposerShufflingDecisionRoot,
78
attesterShufflingDecisionRoot,
89
getBlockRootAtSlot,
@@ -995,7 +996,15 @@ export function getValidatorApi(
995996

996997
// Check that all validatorIndex belong to the state before calling getCommitteeAssignments()
997998
const pubkeys = getPubkeysForIndices(state.validators, indices);
998-
const committeeAssignments = state.epochCtx.getCommitteeAssignments(epoch, indices);
999+
const decisionRoot = state.epochCtx.getShufflingDecisionRoot(epoch);
1000+
const shuffling = await chain.shufflingCache.get(epoch, decisionRoot);
1001+
if (!shuffling) {
1002+
throw new ApiError(
1003+
500,
1004+
`No shuffling found to calculate committee assignments for epoch: ${epoch} and decisionRoot: ${decisionRoot}`
1005+
);
1006+
}
1007+
const committeeAssignments = calculateCommitteeAssignments(shuffling, indices);
9991008
const duties: routes.validator.AttesterDuty[] = [];
10001009
for (let i = 0, len = indices.length; i < len; i++) {
10011010
const validatorIndex = indices[i];

packages/beacon-node/src/chain/blocks/importBlock.ts

-7
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ export async function importBlock(
6464
const blockRootHex = toRootHex(blockRoot);
6565
const currentEpoch = computeEpochAtSlot(this.forkChoice.getTime());
6666
const blockEpoch = computeEpochAtSlot(blockSlot);
67-
const parentEpoch = computeEpochAtSlot(parentBlockSlot);
6867
const prevFinalizedEpoch = this.forkChoice.getFinalizedCheckpoint().epoch;
6968
const blockDelaySec = (fullyVerifiedBlock.seenTimestampSec - postState.genesisTime) % this.config.SECONDS_PER_SLOT;
7069
const recvToValLatency = Date.now() / 1000 - (opts.seenTimestampSec ?? Date.now() / 1000);
@@ -335,12 +334,6 @@ export async function importBlock(
335334
this.logger.verbose("After importBlock caching postState without SSZ cache", {slot: postState.slot});
336335
}
337336

338-
if (parentEpoch < blockEpoch) {
339-
// current epoch and previous epoch are likely cached in previous states
340-
this.shufflingCache.processState(postState, postState.epochCtx.nextShuffling.epoch);
341-
this.logger.verbose("Processed shuffling for next epoch", {parentEpoch, blockEpoch, slot: blockSlot});
342-
}
343-
344337
if (blockSlot % SLOTS_PER_EPOCH === 0) {
345338
// Cache state to preserve epoch transition work
346339
const checkpointState = postState;

packages/beacon-node/src/chain/chain.ts

+18-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
PubkeyIndexMap,
1414
EpochShuffling,
1515
computeEndSlotAtEpoch,
16+
computeAnchorCheckpoint,
1617
} from "@lodestar/state-transition";
1718
import {BeaconConfig} from "@lodestar/config";
1819
import {
@@ -60,7 +61,6 @@ import {
6061
import {IChainOptions} from "./options.js";
6162
import {QueuedStateRegenerator, RegenCaller} from "./regen/index.js";
6263
import {initializeForkChoice} from "./forkChoice/index.js";
63-
import {computeAnchorCheckpoint} from "./initState.js";
6464
import {IBlsVerifier, BlsSingleThreadVerifier, BlsMultiThreadWorkerPool} from "./bls/index.js";
6565
import {
6666
SeenAttesters,
@@ -246,7 +246,6 @@ export class BeaconChain implements IBeaconChain {
246246

247247
this.beaconProposerCache = new BeaconProposerCache(opts);
248248
this.checkpointBalancesCache = new CheckpointBalancesCache();
249-
this.shufflingCache = new ShufflingCache(metrics, this.opts);
250249

251250
// Restore state caches
252251
// anchorState may already by a CachedBeaconState. If so, don't create the cache again, since deserializing all
@@ -261,9 +260,21 @@ export class BeaconChain implements IBeaconChain {
261260
pubkey2index: new PubkeyIndexMap(),
262261
index2pubkey: [],
263262
});
264-
this.shufflingCache.processState(cachedState, cachedState.epochCtx.previousShuffling.epoch);
265-
this.shufflingCache.processState(cachedState, cachedState.epochCtx.currentShuffling.epoch);
266-
this.shufflingCache.processState(cachedState, cachedState.epochCtx.nextShuffling.epoch);
263+
264+
this.shufflingCache = cachedState.epochCtx.shufflingCache = new ShufflingCache(metrics, logger, this.opts, [
265+
{
266+
shuffling: cachedState.epochCtx.previousShuffling,
267+
decisionRoot: cachedState.epochCtx.previousDecisionRoot,
268+
},
269+
{
270+
shuffling: cachedState.epochCtx.currentShuffling,
271+
decisionRoot: cachedState.epochCtx.currentDecisionRoot,
272+
},
273+
{
274+
shuffling: cachedState.epochCtx.nextShuffling,
275+
decisionRoot: cachedState.epochCtx.nextDecisionRoot,
276+
},
277+
]);
267278

268279
// Persist single global instance of state caches
269280
this.pubkey2index = cachedState.epochCtx.pubkey2index;
@@ -902,8 +913,8 @@ export class BeaconChain implements IBeaconChain {
902913
state = await this.regen.getState(attHeadBlock.stateRoot, regenCaller);
903914
}
904915

905-
// resolve the promise to unblock other calls of the same epoch and dependent root
906-
return this.shufflingCache.processState(state, attEpoch);
916+
// should always be the current epoch of the active context so no need to await a result from the ShufflingCache
917+
return state.epochCtx.getShufflingAtEpoch(attEpoch);
907918
}
908919

909920
/**

packages/beacon-node/src/chain/forkChoice/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import {
1414
getEffectiveBalanceIncrementsZeroInactive,
1515
isExecutionStateType,
1616
isMergeTransitionComplete,
17+
computeAnchorCheckpoint,
1718
} from "@lodestar/state-transition";
1819

1920
import {Logger, toRootHex} from "@lodestar/utils";
20-
import {computeAnchorCheckpoint} from "../initState.js";
2121
import {ChainEventEmitter} from "../emitter.js";
2222
import {ChainEvent} from "../emitter.js";
2323
import {GENESIS_SLOT} from "../../constants/index.js";

packages/beacon-node/src/chain/initState.ts

+2-36
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import {
2-
blockToHeader,
32
computeEpochAtSlot,
43
BeaconStateAllForks,
54
CachedBeaconStateAllForks,
6-
computeCheckpointEpochAtStateSlot,
75
computeStartSlotAtEpoch,
86
} from "@lodestar/state-transition";
9-
import {SignedBeaconBlock, phase0, ssz} from "@lodestar/types";
7+
import {SignedBeaconBlock} from "@lodestar/types";
108
import {ChainForkConfig} from "@lodestar/config";
119
import {Logger, toHex, toRootHex} from "@lodestar/utils";
12-
import {GENESIS_SLOT, ZERO_HASH} from "../constants/index.js";
10+
import {GENESIS_SLOT} from "../constants/index.js";
1311
import {IBeaconDb} from "../db/index.js";
1412
import {Eth1Provider} from "../eth1/index.js";
1513
import {Metrics} from "../metrics/index.js";
@@ -204,35 +202,3 @@ export function initBeaconMetrics(metrics: Metrics, state: BeaconStateAllForks):
204202
metrics.currentJustifiedEpoch.set(state.currentJustifiedCheckpoint.epoch);
205203
metrics.finalizedEpoch.set(state.finalizedCheckpoint.epoch);
206204
}
207-
208-
export function computeAnchorCheckpoint(
209-
config: ChainForkConfig,
210-
anchorState: BeaconStateAllForks
211-
): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} {
212-
let blockHeader;
213-
let root;
214-
const blockTypes = config.getForkTypes(anchorState.latestBlockHeader.slot);
215-
216-
if (anchorState.latestBlockHeader.slot === GENESIS_SLOT) {
217-
const block = blockTypes.BeaconBlock.defaultValue();
218-
block.stateRoot = anchorState.hashTreeRoot();
219-
blockHeader = blockToHeader(config, block);
220-
root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader);
221-
} else {
222-
blockHeader = ssz.phase0.BeaconBlockHeader.clone(anchorState.latestBlockHeader);
223-
if (ssz.Root.equals(blockHeader.stateRoot, ZERO_HASH)) {
224-
blockHeader.stateRoot = anchorState.hashTreeRoot();
225-
}
226-
root = ssz.phase0.BeaconBlockHeader.hashTreeRoot(blockHeader);
227-
}
228-
229-
return {
230-
checkpoint: {
231-
root,
232-
// the checkpoint epoch = computeEpochAtSlot(anchorState.slot) + 1 if slot is not at epoch boundary
233-
// this is similar to a process_slots() call
234-
epoch: computeCheckpointEpochAtStateSlot(anchorState.slot),
235-
},
236-
blockHeader,
237-
};
238-
}

0 commit comments

Comments
 (0)