Skip to content

Commit 268c9be

Browse files
feat: add block proposal summary metric to validator monitor (#5603)
* Add block proposal summary metric to validator monitor * Fix type errors * Fix linter error --------- Co-authored-by: Cayman <caymannava@gmail.com>
1 parent 8337608 commit 268c9be

21 files changed

Lines changed: 121 additions & 527 deletions

File tree

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

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {fromHexString} from "@chainsafe/ssz";
22
import {Epoch, Slot, RootHex} from "@lodestar/types";
3-
import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
3+
import {IForkChoice} from "@lodestar/fork-choice";
44
import {Logger, toHex} from "@lodestar/utils";
55
import {ForkSeq, SLOTS_PER_EPOCH, MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS} from "@lodestar/params";
66
import {computeEpochAtSlot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
@@ -18,11 +18,6 @@ const BLOB_SIDECAR_BATCH_SIZE = 32;
1818

1919
type BlockRootSlot = {slot: Slot; root: Uint8Array};
2020
type CheckpointHex = {epoch: Epoch; rootHex: RootHex};
21-
export type FinalizedData = {
22-
finalizedCanonicalCheckpoints: CheckpointHex[];
23-
finalizedCanonicalBlocks: ProtoBlock[];
24-
finalizedNonCanonicalBlocks: ProtoBlock[];
25-
};
2621

2722
/**
2823
* Archives finalized blocks from active bucket to archive bucket.
@@ -41,7 +36,7 @@ export async function archiveBlocks(
4136
logger: Logger,
4237
finalizedCheckpoint: CheckpointHex,
4338
currentEpoch: Epoch
44-
): Promise<FinalizedData> {
39+
): Promise<void> {
4540
// Use fork choice to determine the blocks to archive and delete
4641
// getAllAncestorBlocks response includes the finalized block, so it's also moved to the cold db
4742
const finalizedCanonicalBlocks = forkChoice.getAllAncestorBlocks(finalizedCheckpoint.rootHex);
@@ -103,8 +98,7 @@ export async function archiveBlocks(
10398
}
10499

105100
// Prunning potential checkpoint data
106-
const {nonCheckpointBlocks: finalizedCanonicalNonCheckpointBlocks, checkpoints: finalizedCanonicalCheckpoints} =
107-
getNonCheckpointBlocks(finalizedCanonicalBlockRoots);
101+
const finalizedCanonicalNonCheckpointBlocks = getNonCheckpointBlocks(finalizedCanonicalBlockRoots);
108102
const nonCheckpointBlockRoots: Uint8Array[] = [...nonCanonicalBlockRoots];
109103
for (const block of finalizedCanonicalNonCheckpointBlocks) {
110104
nonCheckpointBlockRoots.push(block.root);
@@ -116,15 +110,6 @@ export async function archiveBlocks(
116110
totalArchived: finalizedCanonicalBlocks.length,
117111
finalizedEpoch: finalizedCheckpoint.epoch,
118112
});
119-
120-
return {
121-
finalizedCanonicalCheckpoints: finalizedCanonicalCheckpoints.map(({root, epoch}) => ({
122-
rootHex: toHex(root),
123-
epoch,
124-
})),
125-
finalizedCanonicalBlocks,
126-
finalizedNonCanonicalBlocks,
127-
};
128113
}
129114

130115
async function migrateBlocksFromHotToColdDb(db: IBeaconDb, blocks: BlockRootSlot[]): Promise<void> {
@@ -222,21 +207,18 @@ export function getParentRootFromSignedBlock(bytes: Uint8Array): Uint8Array {
222207
* @param blocks sequence of linear blocks, from child to ancestor.
223208
* In ProtoArray.getAllAncestorNodes child nodes are pushed first to the returned array.
224209
*/
225-
export function getNonCheckpointBlocks<T extends {slot: Slot}>(
226-
blocks: T[]
227-
): {checkpoints: (T & {epoch: Epoch})[]; nonCheckpointBlocks: T[]} {
210+
export function getNonCheckpointBlocks<T extends {slot: Slot}>(blocks: T[]): T[] {
228211
// Iterate from lowest child to highest ancestor
229212
// Look for the checkpoint of the lowest epoch
230213
// If block at `epoch * SLOTS_PER_EPOCH`, it's a checkpoint.
231214
// - Then for the previous epoch all blocks but the 0 are NOT checkpoints
232215
// - Otherwise for the previous epoch the last block is a checkpoint
233216

234217
if (blocks.length < 1) {
235-
return {checkpoints: [], nonCheckpointBlocks: []};
218+
return [];
236219
}
237220

238221
const nonCheckpointBlocks: T[] = [];
239-
const checkpoints: (T & {epoch: Epoch})[] = [];
240222
// Start with Infinity to always trigger `blockEpoch < epochPtr` in the first loop
241223
let epochPtr = Infinity;
242224
// Assume worst case, since it's unknown if a future epoch will skip the first slot or not.
@@ -268,10 +250,8 @@ export function getNonCheckpointBlocks<T extends {slot: Slot}>(
268250

269251
if (!isCheckpoint) {
270252
nonCheckpointBlocks.push(block);
271-
} else {
272-
checkpoints.push({...block, epoch: epochPtrHasFirstSlot ? blockEpoch : blockEpoch + 1});
273253
}
274254
}
275255

276-
return {nonCheckpointBlocks, checkpoints};
256+
return nonCheckpointBlocks;
277257
}
Lines changed: 5 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import {Logger, LogLevel} from "@lodestar/utils";
2-
import {CheckpointWithHex, IForkChoice} from "@lodestar/fork-choice";
3-
import {ValidatorIndex, Slot} from "@lodestar/types";
4-
import {SLOTS_PER_EPOCH} from "@lodestar/params";
5-
1+
import {Logger} from "@lodestar/utils";
2+
import {CheckpointWithHex} from "@lodestar/fork-choice";
63
import {IBeaconDb} from "../../db/index.js";
74
import {JobItemQueue} from "../../util/queue/index.js";
85
import {IBeaconChain} from "../interface.js";
96
import {ChainEvent} from "../emitter.js";
10-
import {Metrics} from "../../metrics/metrics.js";
11-
import {IStateRegenerator} from "../regen/interface.js";
127
import {StatesArchiver, StatesArchiverOpts} from "./archiveStates.js";
13-
import {archiveBlocks, FinalizedData} from "./archiveBlocks.js";
8+
import {archiveBlocks} from "./archiveBlocks.js";
149

1510
const PROCESS_FINALIZED_CHECKPOINT_QUEUE_LEN = 256;
1611

@@ -48,8 +43,7 @@ export class Archiver {
4843
private readonly chain: IBeaconChain,
4944
private readonly logger: Logger,
5045
signal: AbortSignal,
51-
opts: ArchiverOpts,
52-
private readonly metrics: Metrics | null
46+
opts: ArchiverOpts
5347
) {
5448
this.statesArchiver = new StatesArchiver(chain.regen, db, logger, opts);
5549
this.prevFinalized = chain.forkChoice.getFinalizedCheckpoint();
@@ -95,7 +89,7 @@ export class Archiver {
9589
try {
9690
const finalizedEpoch = finalized.epoch;
9791
this.logger.verbose("Start processing finalized checkpoint", {epoch: finalizedEpoch, rootHex: finalized.rootHex});
98-
const finalizedData = await archiveBlocks(
92+
await archiveBlocks(
9993
this.chain.config,
10094
this.db,
10195
this.chain.forkChoice,
@@ -104,14 +98,6 @@ export class Archiver {
10498
finalized,
10599
this.chain.clock.currentEpoch
106100
);
107-
this.collectFinalizedProposalStats(
108-
this.chain.regen,
109-
this.chain.forkChoice,
110-
this.chain.beaconProposerCache,
111-
finalizedData,
112-
finalized,
113-
this.prevFinalized
114-
);
115101
this.prevFinalized = finalized;
116102

117103
// should be after ArchiveBlocksTask to handle restart cleanly
@@ -174,170 +160,4 @@ export class Archiver {
174160
this.logger.error("Error updating backfilledRanges on finalization", {epoch: finalized.epoch}, e as Error);
175161
}
176162
};
177-
178-
private collectFinalizedProposalStats(
179-
regen: IStateRegenerator,
180-
forkChoice: IForkChoice,
181-
beaconProposerCache: IBeaconChain["beaconProposerCache"],
182-
finalizedData: FinalizedData,
183-
finalized: CheckpointWithHex,
184-
lastFinalized: CheckpointWithHex
185-
): FinalizedStats {
186-
const {finalizedCanonicalCheckpoints, finalizedCanonicalBlocks, finalizedNonCanonicalBlocks} = finalizedData;
187-
188-
// Range to consider is:
189-
// lastFinalized.epoch * SLOTS_PER_EPOCH + 1, .... finalized.epoch * SLOTS_PER_EPOCH
190-
// So we need to check proposer of lastFinalized (index 1 onwards) as well as 0th index proposer
191-
// of current finalized
192-
const finalizedProposersCheckpoints: FinalizedData["finalizedCanonicalCheckpoints"] =
193-
finalizedCanonicalCheckpoints.filter(
194-
(hexCheck) => hexCheck.epoch < finalized.epoch && hexCheck.epoch > lastFinalized.epoch
195-
);
196-
finalizedProposersCheckpoints.push(lastFinalized);
197-
finalizedProposersCheckpoints.push(finalized);
198-
199-
// Sort the data to in following structure to make inferences
200-
const slotProposers = new Map<Slot, {canonicalVals: ValidatorIndex[]; nonCanonicalVals: ValidatorIndex[]}>();
201-
202-
// 1. Process canonical blocks
203-
for (const block of finalizedCanonicalBlocks) {
204-
// simply set to the single entry as no double proposal can be there for same slot in canonical
205-
slotProposers.set(block.slot, {canonicalVals: [block.proposerIndex], nonCanonicalVals: []});
206-
}
207-
208-
// 2. Process non canonical blocks
209-
for (const block of finalizedNonCanonicalBlocks) {
210-
const slotVals = slotProposers.get(block.slot) ?? {canonicalVals: [], nonCanonicalVals: []};
211-
slotVals.nonCanonicalVals.push(block.proposerIndex);
212-
slotProposers.set(block.slot, slotVals);
213-
}
214-
215-
// Some simple calculatable stats for all validators
216-
const finalizedCanonicalCheckpointsCount = finalizedCanonicalCheckpoints.length;
217-
const expectedTotalProposalsCount = (finalized.epoch - lastFinalized.epoch) * SLOTS_PER_EPOCH;
218-
const finalizedCanonicalBlocksCount = finalizedCanonicalBlocks.length;
219-
const finalizedOrphanedProposalsCount = finalizedNonCanonicalBlocks.length;
220-
const finalizedMissedProposalsCount = expectedTotalProposalsCount - slotProposers.size;
221-
222-
const allValidators: ProposalStats = {
223-
total: expectedTotalProposalsCount,
224-
finalized: finalizedCanonicalBlocksCount,
225-
orphaned: finalizedOrphanedProposalsCount,
226-
missed: finalizedMissedProposalsCount,
227-
};
228-
229-
// Stats about the attached validators
230-
const attachedProposers = beaconProposerCache
231-
.getProposersSinceEpoch(finalized.epoch)
232-
.map((indexString) => Number(indexString));
233-
const finalizedAttachedValidatorsCount = attachedProposers.length;
234-
235-
// Calculate stats for attached validators, based on states in checkpointState cache
236-
let finalizedFoundCheckpointsInStateCache = 0;
237-
238-
let expectedAttachedValidatorsProposalsCount = 0;
239-
let finalizedAttachedValidatorsProposalsCount = 0;
240-
let finalizedAttachedValidatorsOrphanCount = 0;
241-
let finalizedAttachedValidatorsMissedCount = 0;
242-
243-
for (const checkpointHex of finalizedProposersCheckpoints) {
244-
const checkpointState = regen.getCheckpointStateSync(checkpointHex);
245-
246-
// Generate stats for attached validators if we have state info
247-
if (checkpointState !== null) {
248-
finalizedFoundCheckpointsInStateCache++;
249-
250-
const epochProposers = checkpointState.epochCtx.proposers;
251-
const startSlot = checkpointState.epochCtx.epoch * SLOTS_PER_EPOCH;
252-
253-
for (let index = 0; index < epochProposers.length; index++) {
254-
const slot = startSlot + index;
255-
256-
// Let skip processing the slots which are out of range
257-
// Range to consider is:
258-
// lastFinalized.epoch * SLOTS_PER_EPOCH + 1, .... finalized.epoch * SLOTS_PER_EPOCH
259-
if (slot <= lastFinalized.epoch * SLOTS_PER_EPOCH || slot > finalized.epoch * SLOTS_PER_EPOCH) {
260-
continue;
261-
}
262-
263-
const proposer = epochProposers[index];
264-
265-
// If this proposer was attached to this BN for this epoch
266-
if (attachedProposers.includes(proposer)) {
267-
expectedAttachedValidatorsProposalsCount++;
268-
269-
// Get what validators made canonical/non canonical proposals for this slot
270-
const {canonicalVals, nonCanonicalVals} = slotProposers.get(slot) ?? {
271-
canonicalVals: [],
272-
nonCanonicalVals: [],
273-
};
274-
let wasFinalized = false;
275-
276-
if (canonicalVals.includes(proposer)) {
277-
finalizedAttachedValidatorsProposalsCount++;
278-
wasFinalized = true;
279-
}
280-
const attachedProposerNonCanSlotProposals = nonCanonicalVals.filter((nonCanVal) => nonCanVal === proposer);
281-
finalizedAttachedValidatorsOrphanCount += attachedProposerNonCanSlotProposals.length;
282-
283-
// Check is this slot proposal was missed by this attached validator
284-
if (!wasFinalized && attachedProposerNonCanSlotProposals.length === 0) {
285-
finalizedAttachedValidatorsMissedCount++;
286-
}
287-
}
288-
}
289-
}
290-
}
291-
292-
const attachedValidators: ProposalStats = {
293-
total: expectedAttachedValidatorsProposalsCount,
294-
finalized: finalizedAttachedValidatorsProposalsCount,
295-
orphaned: finalizedAttachedValidatorsOrphanCount,
296-
missed: finalizedAttachedValidatorsMissedCount,
297-
};
298-
299-
this.logger.debug("All validators finalized proposal stats", {
300-
...allValidators,
301-
finalizedCanonicalCheckpointsCount,
302-
finalizedFoundCheckpointsInStateCache,
303-
});
304-
305-
// Only log to info if there is some relevant data to show
306-
// - No need to explicitly track SYNCED state since no validators attached would be there to show
307-
// - debug log if validators attached but no proposals were scheduled
308-
// - info log if proposals were scheduled (canonical) or there were orphans (non canonical)
309-
if (finalizedAttachedValidatorsCount !== 0) {
310-
const logLevel =
311-
attachedValidators.total !== 0 || attachedValidators.orphaned !== 0 ? LogLevel.info : LogLevel.debug;
312-
this.logger[logLevel]("Attached validators finalized proposal stats", {
313-
...attachedValidators,
314-
validators: finalizedAttachedValidatorsCount,
315-
});
316-
} else {
317-
this.logger.debug("No proposers attached to beacon node", {finalizedEpoch: finalized.epoch});
318-
}
319-
320-
this.metrics?.allValidators.total.set(allValidators.total);
321-
this.metrics?.allValidators.finalized.set(allValidators.finalized);
322-
this.metrics?.allValidators.orphaned.set(allValidators.orphaned);
323-
this.metrics?.allValidators.missed.set(allValidators.missed);
324-
325-
this.metrics?.attachedValidators.total.set(attachedValidators.total);
326-
this.metrics?.attachedValidators.finalized.set(attachedValidators.finalized);
327-
this.metrics?.attachedValidators.orphaned.set(attachedValidators.orphaned);
328-
this.metrics?.attachedValidators.missed.set(attachedValidators.missed);
329-
330-
this.metrics?.finalizedCanonicalCheckpointsCount.set(finalizedCanonicalCheckpointsCount);
331-
this.metrics?.finalizedFoundCheckpointsInStateCache.set(finalizedFoundCheckpointsInStateCache);
332-
this.metrics?.finalizedAttachedValidatorsCount.set(finalizedAttachedValidatorsCount);
333-
334-
// Return stats data for the ease of unit testing
335-
return {
336-
allValidators,
337-
attachedValidators,
338-
finalizedCanonicalCheckpointsCount,
339-
finalizedFoundCheckpointsInStateCache,
340-
finalizedAttachedValidatorsCount,
341-
};
342-
}
343163
}

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

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,16 @@ const PROPOSER_PRESERVE_EPOCHS = 2;
88
export type ProposerPreparationData = routes.validator.ProposerPreparationData;
99

1010
export class BeaconProposerCache {
11-
private readonly feeRecipientByValidatorIndex: MapDef<
12-
string,
13-
{epoch: Epoch; feeRecipient: string; sinceEpoch: Epoch}
14-
>;
11+
private readonly feeRecipientByValidatorIndex: MapDef<string, {epoch: Epoch; feeRecipient: string}>;
1512
constructor(opts: {suggestedFeeRecipient: string}, private readonly metrics?: Metrics | null) {
16-
this.feeRecipientByValidatorIndex = new MapDef<string, {epoch: Epoch; feeRecipient: string; sinceEpoch: Epoch}>(
17-
() => ({
18-
epoch: 0,
19-
feeRecipient: opts.suggestedFeeRecipient,
20-
sinceEpoch: 0,
21-
})
22-
);
13+
this.feeRecipientByValidatorIndex = new MapDef(() => ({
14+
epoch: 0,
15+
feeRecipient: opts.suggestedFeeRecipient,
16+
}));
2317
}
2418

2519
add(epoch: Epoch, {validatorIndex, feeRecipient}: ProposerPreparationData): void {
26-
const sinceEpoch = this.feeRecipientByValidatorIndex.get(validatorIndex)?.sinceEpoch ?? epoch;
27-
this.feeRecipientByValidatorIndex.set(validatorIndex, {epoch, feeRecipient, sinceEpoch});
20+
this.feeRecipientByValidatorIndex.set(validatorIndex, {epoch, feeRecipient});
2821
}
2922

3023
prune(epoch: Epoch): void {
@@ -44,14 +37,4 @@ export class BeaconProposerCache {
4437
get(proposerIndex: number | string): string | undefined {
4538
return this.feeRecipientByValidatorIndex.get(`${proposerIndex}`)?.feeRecipient;
4639
}
47-
48-
getProposersSinceEpoch(epoch: Epoch): ProposerPreparationData["validatorIndex"][] {
49-
const proposers = [];
50-
for (const [validatorIndex, feeRecipientEntry] of this.feeRecipientByValidatorIndex.entries()) {
51-
if (feeRecipientEntry.sinceEpoch <= epoch) {
52-
proposers.push(validatorIndex);
53-
}
54-
}
55-
return proposers;
56-
}
5740
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ export class BeaconChain implements IBeaconChain {
275275
this.emitter = emitter;
276276
this.lightClientServer = lightClientServer;
277277

278-
this.archiver = new Archiver(db, this, logger, signal, opts, metrics);
278+
this.archiver = new Archiver(db, this, logger, signal, opts);
279279
// always run PrepareNextSlotScheduler except for fork_choice spec tests
280280
if (!opts?.disablePrepareNextSlot) {
281281
new PrepareNextSlotScheduler(this, this.config, metrics, this.logger, signal);

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ export function initializeForkChoice(
6565
ProtoArray.initialize(
6666
{
6767
slot: blockHeader.slot,
68-
proposerIndex: blockHeader.proposerIndex,
6968
parentRoot: toHexString(blockHeader.parentRoot),
7069
stateRoot: toHexString(blockHeader.stateRoot),
7170
blockRoot: toHexString(checkpoint.root),

0 commit comments

Comments
 (0)