@aztec/aztec-node 0.0.1-commit.b655e406 → 0.0.1-commit.c0b82b2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,12 +20,7 @@ export async function createSentinel(
20
20
  if (!config.sentinelEnabled) {
21
21
  return undefined;
22
22
  }
23
- const kvStore = await createStore(
24
- 'sentinel',
25
- SentinelStore.SCHEMA_VERSION,
26
- config,
27
- createLogger('node:sentinel:lmdb'),
28
- );
23
+ const kvStore = await createStore('sentinel', SentinelStore.SCHEMA_VERSION, config, logger.getBindings());
29
24
  const storeHistoryLength = config.sentinelHistoryLengthInEpochs * epochCache.getL1Constants().epochDuration;
30
25
  const storeHistoricProvenPerformanceLength = config.sentinelHistoricProvenPerformanceLengthInEpochs;
31
26
  const sentinelStore = new SentinelStore(kvStore, {
@@ -1,4 +1,5 @@
1
1
  import type { EpochCache } from '@aztec/epoch-cache';
2
+ import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
2
3
  import { countWhile, filterAsync, fromEntries, getEntries, mapValues } from '@aztec/foundation/collection';
3
4
  import { EthAddress } from '@aztec/foundation/eth-address';
4
5
  import { createLogger } from '@aztec/foundation/log';
@@ -18,7 +19,7 @@ import {
18
19
  L2BlockStream,
19
20
  type L2BlockStreamEvent,
20
21
  type L2BlockStreamEventHandler,
21
- getAttestationInfoFromPublishedL2Block,
22
+ getAttestationInfoFromPublishedCheckpoint,
22
23
  } from '@aztec/stdlib/block';
23
24
  import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
24
25
  import type {
@@ -35,15 +36,29 @@ import EventEmitter from 'node:events';
35
36
 
36
37
  import { SentinelStore } from './store.js';
37
38
 
39
+ /** Maps a validator status to its category: proposer or attestation. */
40
+ function statusToCategory(status: ValidatorStatusInSlot): ValidatorStatusType {
41
+ switch (status) {
42
+ case 'attestation-sent':
43
+ case 'attestation-missed':
44
+ return 'attestation';
45
+ default:
46
+ return 'proposer';
47
+ }
48
+ }
49
+
38
50
  export class Sentinel extends (EventEmitter as new () => WatcherEmitter) implements L2BlockStreamEventHandler, Watcher {
39
51
  protected runningPromise: RunningPromise;
40
52
  protected blockStream!: L2BlockStream;
41
53
  protected l2TipsStore: L2TipsStore;
42
54
 
43
- protected initialSlot: bigint | undefined;
44
- protected lastProcessedSlot: bigint | undefined;
45
- protected slotNumberToBlock: Map<bigint, { blockNumber: number; archive: string; attestors: EthAddress[] }> =
46
- new Map();
55
+ protected initialSlot: SlotNumber | undefined;
56
+ protected lastProcessedSlot: SlotNumber | undefined;
57
+ // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
58
+ protected slotNumberToCheckpoint: Map<
59
+ SlotNumber,
60
+ { checkpointNumber: CheckpointNumber; archive: string; attestors: EthAddress[] }
61
+ > = new Map();
47
62
 
48
63
  constructor(
49
64
  protected epochCache: EpochCache,
@@ -74,7 +89,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
74
89
  /** Loads initial slot and initializes blockstream. We will not process anything at or before the initial slot. */
75
90
  protected async init() {
76
91
  this.initialSlot = this.epochCache.getEpochAndSlotNow().slot;
77
- const startingBlock = await this.archiver.getBlockNumber();
92
+ const startingBlock = BlockNumber(await this.archiver.getBlockNumber());
78
93
  this.logger.info(`Starting validator sentinel with initial slot ${this.initialSlot} and block ${startingBlock}`);
79
94
  this.blockStream = new L2BlockStream(this.archiver, this.l2TipsStore, this, this.logger, { startingBlock });
80
95
  }
@@ -85,47 +100,54 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
85
100
 
86
101
  public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
87
102
  await this.l2TipsStore.handleBlockStreamEvent(event);
88
- if (event.type === 'blocks-added') {
89
- // Store mapping from slot to archive, block number, and attestors
90
- for (const block of event.blocks) {
91
- this.slotNumberToBlock.set(block.block.header.getSlot(), {
92
- blockNumber: block.block.number,
93
- archive: block.block.archive.root.toString(),
94
- attestors: getAttestationInfoFromPublishedL2Block(block)
95
- .filter(a => a.status === 'recovered-from-signature')
96
- .map(a => a.address!),
97
- });
98
- }
99
-
100
- // Prune the archive map to only keep at most N entries
101
- const historyLength = this.store.getHistoryLength();
102
- if (this.slotNumberToBlock.size > historyLength) {
103
- const toDelete = Array.from(this.slotNumberToBlock.keys())
104
- .sort((a, b) => Number(a - b))
105
- .slice(0, this.slotNumberToBlock.size - historyLength);
106
- for (const key of toDelete) {
107
- this.slotNumberToBlock.delete(key);
108
- }
109
- }
103
+ if (event.type === 'chain-checkpointed') {
104
+ this.handleCheckpoint(event);
110
105
  } else if (event.type === 'chain-proven') {
111
106
  await this.handleChainProven(event);
112
107
  }
113
108
  }
114
109
 
110
+ protected handleCheckpoint(event: L2BlockStreamEvent) {
111
+ if (event.type !== 'chain-checkpointed') {
112
+ return;
113
+ }
114
+ const checkpoint = event.checkpoint;
115
+
116
+ // Store mapping from slot to archive, checkpoint number, and attestors
117
+ this.slotNumberToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, {
118
+ checkpointNumber: checkpoint.checkpoint.number,
119
+ archive: checkpoint.checkpoint.archive.root.toString(),
120
+ attestors: getAttestationInfoFromPublishedCheckpoint(checkpoint)
121
+ .filter(a => a.status === 'recovered-from-signature')
122
+ .map(a => a.address!),
123
+ });
124
+
125
+ // Prune the archive map to only keep at most N entries
126
+ const historyLength = this.store.getHistoryLength();
127
+ if (this.slotNumberToCheckpoint.size > historyLength) {
128
+ const toDelete = Array.from(this.slotNumberToCheckpoint.keys())
129
+ .sort((a, b) => Number(a - b))
130
+ .slice(0, this.slotNumberToCheckpoint.size - historyLength);
131
+ for (const key of toDelete) {
132
+ this.slotNumberToCheckpoint.delete(key);
133
+ }
134
+ }
135
+ }
136
+
115
137
  protected async handleChainProven(event: L2BlockStreamEvent) {
116
138
  if (event.type !== 'chain-proven') {
117
139
  return;
118
140
  }
119
141
  const blockNumber = event.block.number;
120
- const block = await this.archiver.getBlock(blockNumber);
121
- if (!block) {
122
- this.logger.error(`Failed to get block ${blockNumber}`, { block });
142
+ const header = await this.archiver.getBlockHeader(blockNumber);
143
+ if (!header) {
144
+ this.logger.error(`Failed to get block header ${blockNumber}`);
123
145
  return;
124
146
  }
125
147
 
126
148
  // TODO(palla/slash): We should only be computing proven performance if this is
127
149
  // a full proof epoch and not a partial one, otherwise we'll end up with skewed stats.
128
- const epoch = getEpochAtSlot(block.header.getSlot(), this.epochCache.getL1Constants());
150
+ const epoch = getEpochAtSlot(header.getSlot(), this.epochCache.getL1Constants());
129
151
  this.logger.debug(`Computing proven performance for epoch ${epoch}`);
130
152
  const performance = await this.computeProvenPerformance(epoch);
131
153
  this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
@@ -134,15 +156,23 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
134
156
  await this.handleProvenPerformance(epoch, performance);
135
157
  }
136
158
 
137
- protected async computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance> {
159
+ protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
138
160
  const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
139
- const { committee } = await this.epochCache.getCommittee(fromSlot);
161
+ const { committee, isEscapeHatchOpen } = await this.epochCache.getCommittee(fromSlot);
162
+ if (isEscapeHatchOpen) {
163
+ this.logger.info(`Skipping proven performance for epoch ${epoch} - escape hatch is open`);
164
+ return {};
165
+ }
140
166
  if (!committee) {
141
167
  this.logger.trace(`No committee found for slot ${fromSlot}`);
142
168
  return {};
143
169
  }
144
170
 
145
- const stats = await this.computeStats({ fromSlot, toSlot, validators: committee });
171
+ const stats = await this.computeStats({
172
+ fromSlot,
173
+ toSlot,
174
+ validators: committee,
175
+ });
146
176
  this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
147
177
 
148
178
  // Note that we are NOT using the total slots in the epoch as `total` here, since we only
@@ -165,7 +195,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
165
195
  */
166
196
  protected async checkPastInactivity(
167
197
  validator: EthAddress,
168
- currentEpoch: bigint,
198
+ currentEpoch: EpochNumber,
169
199
  requiredConsecutiveEpochs: number,
170
200
  ): Promise<boolean> {
171
201
  if (requiredConsecutiveEpochs === 0) {
@@ -175,23 +205,24 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
175
205
  // Get all historical performance for this validator
176
206
  const allPerformance = await this.store.getProvenPerformance(validator);
177
207
 
208
+ // Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
209
+ const pastEpochs = allPerformance.sort((a, b) => Number(b.epoch - a.epoch)).filter(p => p.epoch < currentEpoch);
210
+
178
211
  // If we don't have enough historical data, don't slash
179
- if (allPerformance.length < requiredConsecutiveEpochs) {
212
+ if (pastEpochs.length < requiredConsecutiveEpochs) {
180
213
  this.logger.debug(
181
214
  `Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
182
215
  );
183
216
  return false;
184
217
  }
185
218
 
186
- // Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
187
- return allPerformance
188
- .sort((a, b) => Number(b.epoch - a.epoch))
189
- .filter(p => p.epoch < currentEpoch)
219
+ // Check that we have at least requiredConsecutiveEpochs and that all of them are above the inactivity threshold
220
+ return pastEpochs
190
221
  .slice(0, requiredConsecutiveEpochs)
191
222
  .every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
192
223
  }
193
224
 
194
- protected async handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
225
+ protected async handleProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
195
226
  if (this.config.slashInactivityPenalty === 0n) {
196
227
  return;
197
228
  }
@@ -215,7 +246,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
215
246
  validator: EthAddress.fromString(address),
216
247
  amount: this.config.slashInactivityPenalty,
217
248
  offenseType: OffenseType.INACTIVITY,
218
- epochOrSlot: epoch,
249
+ epochOrSlot: BigInt(epoch),
219
250
  }));
220
251
 
221
252
  if (criminals.length > 0) {
@@ -256,8 +287,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
256
287
  * We also don't move past the archiver last synced L2 slot, as we don't want to process data that is not yet available.
257
288
  * Last, we check the p2p is synced with the archiver, so it has pulled all attestations from it.
258
289
  */
259
- protected async isReadyToProcess(currentSlot: bigint) {
260
- const targetSlot = currentSlot - 2n;
290
+ protected async isReadyToProcess(currentSlot: SlotNumber): Promise<SlotNumber | false> {
291
+ if (currentSlot < 2) {
292
+ this.logger.trace(`Current slot ${currentSlot} too early.`);
293
+ return false;
294
+ }
295
+
296
+ const targetSlot = SlotNumber(currentSlot - 2);
261
297
  if (this.lastProcessedSlot && this.lastProcessedSlot >= targetSlot) {
262
298
  this.logger.trace(`Already processed slot ${targetSlot}`, { lastProcessedSlot: this.lastProcessedSlot });
263
299
  return false;
@@ -279,8 +315,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
279
315
  return false;
280
316
  }
281
317
 
282
- const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.latest.hash);
283
- const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.latest.hash);
318
+ const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.proposed.hash);
319
+ const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.proposed.hash);
284
320
  const isP2pSynced = archiverLastBlockHash === p2pLastBlockHash;
285
321
  if (!isP2pSynced) {
286
322
  this.logger.debug(`Waiting for P2P client to sync with archiver`, { archiverLastBlockHash, p2pLastBlockHash });
@@ -294,8 +330,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
294
330
  * Gathers committee and proposer data for a given slot, computes slot stats,
295
331
  * and updates overall stats.
296
332
  */
297
- protected async processSlot(slot: bigint) {
298
- const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
333
+ protected async processSlot(slot: SlotNumber) {
334
+ const { epoch, seed, committee, isEscapeHatchOpen } = await this.epochCache.getCommittee(slot);
335
+ if (isEscapeHatchOpen) {
336
+ this.logger.info(`Skipping slot ${slot} at epoch ${epoch} - escape hatch is open`);
337
+ this.lastProcessedSlot = slot;
338
+ return;
339
+ }
299
340
  if (!committee || committee.length === 0) {
300
341
  this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
301
342
  this.lastProcessedSlot = slot;
@@ -310,21 +351,21 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
310
351
  }
311
352
 
312
353
  /** Computes activity for a given slot. */
313
- protected async getSlotActivity(slot: bigint, epoch: bigint, proposer: EthAddress, committee: EthAddress[]) {
354
+ protected async getSlotActivity(slot: SlotNumber, epoch: EpochNumber, proposer: EthAddress, committee: EthAddress[]) {
314
355
  this.logger.debug(`Computing stats for slot ${slot} at epoch ${epoch}`, { slot, epoch, proposer, committee });
315
356
 
316
357
  // Check if there is an L2 block in L1 for this L2 slot
317
358
 
318
- // Here we get all attestations for the block mined at the given slot,
319
- // or all attestations for all proposals in the slot if no block was mined.
359
+ // Here we get all checkpoint attestations for the checkpoint at the given slot,
360
+ // or all checkpoint attestations for all proposals in the slot if no checkpoint was mined.
320
361
  // We gather from both p2p (contains the ones seen on the p2p layer) and archiver
321
- // (contains the ones synced from mined blocks, which we may have missed from p2p).
322
- const block = this.slotNumberToBlock.get(slot);
323
- const p2pAttested = await this.p2p.getAttestationsForSlot(slot, block?.archive);
362
+ // (contains the ones synced from mined checkpoints, which we may have missed from p2p).
363
+ const checkpoint = this.slotNumberToCheckpoint.get(slot);
364
+ const p2pAttested = await this.p2p.getCheckpointAttestationsForSlot(slot, checkpoint?.archive);
324
365
  // Filter out attestations with invalid signatures
325
366
  const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
326
367
  const attestors = new Set(
327
- [...p2pAttestors.map(a => a.toString()), ...(block?.attestors.map(a => a.toString()) ?? [])].filter(
368
+ [...p2pAttestors.map(a => a.toString()), ...(checkpoint?.attestors.map(a => a.toString()) ?? [])].filter(
328
369
  addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
329
370
  ),
330
371
  );
@@ -335,20 +376,29 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
335
376
  // But we'll leave that corner case out to reduce pressure on the node.
336
377
  // TODO(palla/slash): This breaks if a given node has more than one validator in the current committee,
337
378
  // since they will attest to their own proposal it even if it's not re-executable.
338
- const blockStatus = block ? 'mined' : attestors.size > 0 ? 'proposed' : 'missed';
339
- this.logger.debug(`Block for slot ${slot} was ${blockStatus}`, { ...block, slot });
379
+ let status: 'checkpoint-mined' | 'checkpoint-proposed' | 'checkpoint-missed' | 'blocks-missed';
380
+ if (checkpoint) {
381
+ status = 'checkpoint-mined';
382
+ } else if (attestors.size > 0) {
383
+ status = 'checkpoint-proposed';
384
+ } else {
385
+ // No checkpoint on L1 and no checkpoint attestations seen. Check if block proposals were sent for this slot.
386
+ const hasBlockProposals = await this.p2p.hasBlockProposalsForSlot(slot);
387
+ status = hasBlockProposals ? 'checkpoint-missed' : 'blocks-missed';
388
+ }
389
+ this.logger.debug(`Checkpoint status for slot ${slot}: ${status}`, { ...checkpoint, slot });
340
390
 
341
- // Get attestors that failed their duties for this block, but only if there was a block proposed
391
+ // Get attestors that failed their checkpoint attestation duties, but only if there was a checkpoint proposed or mined
342
392
  const missedAttestors = new Set(
343
- blockStatus === 'missed'
393
+ status === 'blocks-missed' || status === 'checkpoint-missed'
344
394
  ? []
345
395
  : committee.filter(v => !attestors.has(v.toString()) && !proposer.equals(v)).map(v => v.toString()),
346
396
  );
347
397
 
348
398
  this.logger.debug(`Retrieved ${attestors.size} attestors out of ${committee.length} for slot ${slot}`, {
349
- blockStatus,
399
+ status,
350
400
  proposer: proposer.toString(),
351
- ...block,
401
+ ...checkpoint,
352
402
  slot,
353
403
  attestors: [...attestors],
354
404
  missedAttestors: [...missedAttestors],
@@ -358,7 +408,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
358
408
  // Compute the status for each validator in the committee
359
409
  const statusFor = (who: `0x${string}`): ValidatorStatusInSlot | undefined => {
360
410
  if (who === proposer.toString()) {
361
- return `block-${blockStatus}`;
411
+ return status;
362
412
  } else if (attestors.has(who)) {
363
413
  return 'attestation-sent';
364
414
  } else if (missedAttestors.has(who)) {
@@ -372,7 +422,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
372
422
  }
373
423
 
374
424
  /** Push the status for each slot for each validator. */
375
- protected updateValidators(slot: bigint, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
425
+ protected updateValidators(slot: SlotNumber, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
376
426
  return this.store.updateValidators(slot, stats);
377
427
  }
378
428
 
@@ -381,13 +431,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
381
431
  fromSlot,
382
432
  toSlot,
383
433
  validators,
384
- }: { fromSlot?: bigint; toSlot?: bigint; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
434
+ }: { fromSlot?: SlotNumber; toSlot?: SlotNumber; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
385
435
  const histories = validators
386
436
  ? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
387
437
  : await this.store.getHistories();
388
438
 
389
439
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
390
- fromSlot ??= (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
440
+ fromSlot ??= SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
391
441
  toSlot ??= this.lastProcessedSlot ?? slotNow;
392
442
 
393
443
  const stats = mapValues(histories, (history, address) =>
@@ -405,8 +455,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
405
455
  /** Computes stats for a single validator. */
406
456
  public async getValidatorStats(
407
457
  validatorAddress: EthAddress,
408
- fromSlot?: bigint,
409
- toSlot?: bigint,
458
+ fromSlot?: SlotNumber,
459
+ toSlot?: SlotNumber,
410
460
  ): Promise<SingleValidatorStats | undefined> {
411
461
  const history = await this.store.getHistory(validatorAddress);
412
462
 
@@ -415,13 +465,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
415
465
  }
416
466
 
417
467
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
418
- const effectiveFromSlot = fromSlot ?? (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
468
+ const effectiveFromSlot =
469
+ fromSlot ?? SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
419
470
  const effectiveToSlot = toSlot ?? this.lastProcessedSlot ?? slotNow;
420
471
 
421
472
  const historyLength = BigInt(this.store.getHistoryLength());
422
- if (effectiveToSlot - effectiveFromSlot > historyLength) {
473
+ if (BigInt(effectiveToSlot) - BigInt(effectiveFromSlot) > historyLength) {
423
474
  throw new Error(
424
- `Slot range (${effectiveToSlot - effectiveFromSlot}) exceeds history length (${historyLength}). ` +
475
+ `Slot range (${BigInt(effectiveToSlot) - BigInt(effectiveFromSlot)}) exceeds history length (${historyLength}). ` +
425
476
  `Requested range: ${effectiveFromSlot} to ${effectiveToSlot}.`,
426
477
  );
427
478
  }
@@ -432,11 +483,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
432
483
  effectiveFromSlot,
433
484
  effectiveToSlot,
434
485
  );
435
- const allTimeProvenPerformance = await this.store.getProvenPerformance(validatorAddress);
436
486
 
437
487
  return {
438
488
  validator,
439
- allTimeProvenPerformance,
489
+ allTimeProvenPerformance: await this.store.getProvenPerformance(validatorAddress),
440
490
  lastProcessedSlot: this.lastProcessedSlot,
441
491
  initialSlot: this.initialSlot,
442
492
  slotWindow: this.store.getHistoryLength(),
@@ -446,19 +496,21 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
446
496
  protected computeStatsForValidator(
447
497
  address: `0x${string}`,
448
498
  allHistory: ValidatorStatusHistory,
449
- fromSlot?: bigint,
450
- toSlot?: bigint,
499
+ fromSlot?: SlotNumber,
500
+ toSlot?: SlotNumber,
451
501
  ): ValidatorStats {
452
- let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
453
- history = toSlot ? history.filter(h => h.slot <= toSlot) : history;
454
- const lastProposal = history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
502
+ let history = fromSlot ? allHistory.filter(h => BigInt(h.slot) >= fromSlot) : allHistory;
503
+ history = toSlot ? history.filter(h => BigInt(h.slot) <= toSlot) : history;
504
+ const lastProposal = history
505
+ .filter(h => h.status === 'checkpoint-proposed' || h.status === 'checkpoint-mined')
506
+ .at(-1);
455
507
  const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
456
508
  return {
457
509
  address: EthAddress.fromString(address),
458
510
  lastProposal: this.computeFromSlot(lastProposal?.slot),
459
511
  lastAttestation: this.computeFromSlot(lastAttestation?.slot),
460
512
  totalSlots: history.length,
461
- missedProposals: this.computeMissed(history, 'block', ['block-missed']),
513
+ missedProposals: this.computeMissed(history, 'proposer', ['checkpoint-missed', 'blocks-missed']),
462
514
  missedAttestations: this.computeMissed(history, 'attestation', ['attestation-missed']),
463
515
  history,
464
516
  };
@@ -466,10 +518,12 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
466
518
 
467
519
  protected computeMissed(
468
520
  history: ValidatorStatusHistory,
469
- computeOverPrefix: ValidatorStatusType | undefined,
521
+ computeOverCategory: ValidatorStatusType | undefined,
470
522
  filter: ValidatorStatusInSlot[],
471
523
  ) {
472
- const relevantHistory = history.filter(h => !computeOverPrefix || h.status.startsWith(computeOverPrefix));
524
+ const relevantHistory = history.filter(
525
+ h => !computeOverCategory || statusToCategory(h.status) === computeOverCategory,
526
+ );
473
527
  const filteredHistory = relevantHistory.filter(h => filter.includes(h.status));
474
528
  return {
475
529
  currentStreak: countWhile([...relevantHistory].reverse(), h => filter.includes(h.status)),
@@ -479,7 +533,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
479
533
  };
480
534
  }
481
535
 
482
- protected computeFromSlot(slot: bigint | undefined) {
536
+ protected computeFromSlot(slot: SlotNumber | undefined) {
483
537
  if (slot === undefined) {
484
538
  return undefined;
485
539
  }
@@ -1,3 +1,4 @@
1
+ import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import { EthAddress } from '@aztec/foundation/eth-address';
2
3
  import { BufferReader, numToUInt8, numToUInt32BE, serializeToBuffer } from '@aztec/foundation/serialize';
3
4
  import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
@@ -8,7 +9,7 @@ import type {
8
9
  } from '@aztec/stdlib/validators';
9
10
 
10
11
  export class SentinelStore {
11
- public static readonly SCHEMA_VERSION = 2;
12
+ public static readonly SCHEMA_VERSION = 3;
12
13
 
13
14
  // a map from validator address to their ValidatorStatusHistory
14
15
  private readonly historyMap: AztecAsyncMap<`0x${string}`, Buffer>;
@@ -33,7 +34,7 @@ export class SentinelStore {
33
34
  return this.config.historicProvenPerformanceLength;
34
35
  }
35
36
 
36
- public async updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
37
+ public async updateProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
37
38
  await this.store.transactionAsync(async () => {
38
39
  for (const [who, { missed, total }] of Object.entries(performance)) {
39
40
  await this.pushValidatorProvenPerformanceForEpoch({ who: EthAddress.fromString(who), missed, total, epoch });
@@ -41,7 +42,7 @@ export class SentinelStore {
41
42
  });
42
43
  }
43
44
 
44
- public async getProvenPerformance(who: EthAddress): Promise<{ missed: number; total: number; epoch: bigint }[]> {
45
+ public async getProvenPerformance(who: EthAddress): Promise<{ missed: number; total: number; epoch: EpochNumber }[]> {
45
46
  const currentPerformanceBuffer = await this.provenMap.getAsync(who.toString());
46
47
  return currentPerformanceBuffer ? this.deserializePerformance(currentPerformanceBuffer) : [];
47
48
  }
@@ -55,7 +56,7 @@ export class SentinelStore {
55
56
  who: EthAddress;
56
57
  missed: number;
57
58
  total: number;
58
- epoch: bigint;
59
+ epoch: EpochNumber;
59
60
  }) {
60
61
  const currentPerformance = await this.getProvenPerformance(who);
61
62
  const existingIndex = currentPerformance.findIndex(p => p.epoch === epoch);
@@ -75,7 +76,7 @@ export class SentinelStore {
75
76
  await this.provenMap.set(who.toString(), this.serializePerformance(performanceToKeep));
76
77
  }
77
78
 
78
- public async updateValidators(slot: bigint, statuses: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
79
+ public async updateValidators(slot: SlotNumber, statuses: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
79
80
  await this.store.transactionAsync(async () => {
80
81
  for (const [who, status] of Object.entries(statuses)) {
81
82
  if (status) {
@@ -85,11 +86,7 @@ export class SentinelStore {
85
86
  });
86
87
  }
87
88
 
88
- private async pushValidatorStatusForSlot(
89
- who: EthAddress,
90
- slot: bigint,
91
- status: 'block-mined' | 'block-proposed' | 'block-missed' | 'attestation-sent' | 'attestation-missed',
92
- ) {
89
+ private async pushValidatorStatusForSlot(who: EthAddress, slot: SlotNumber, status: ValidatorStatusInSlot) {
93
90
  await this.store.transactionAsync(async () => {
94
91
  const currentHistory = (await this.getHistory(who)) ?? [];
95
92
  const newHistory = [...currentHistory, { slot, status }].slice(-this.config.historyLength);
@@ -110,18 +107,18 @@ export class SentinelStore {
110
107
  return data && this.deserializeHistory(data);
111
108
  }
112
109
 
113
- private serializePerformance(performance: { missed: number; total: number; epoch: bigint }[]): Buffer {
110
+ private serializePerformance(performance: { missed: number; total: number; epoch: EpochNumber }[]): Buffer {
114
111
  return serializeToBuffer(
115
112
  performance.map(p => [numToUInt32BE(Number(p.epoch)), numToUInt32BE(p.missed), numToUInt32BE(p.total)]),
116
113
  );
117
114
  }
118
115
 
119
- private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: bigint }[] {
116
+ private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: EpochNumber }[] {
120
117
  const reader = new BufferReader(buffer);
121
- const performance: { missed: number; total: number; epoch: bigint }[] = [];
118
+ const performance: { missed: number; total: number; epoch: EpochNumber }[] = [];
122
119
  while (!reader.isEmpty()) {
123
120
  performance.push({
124
- epoch: BigInt(reader.readNumber()),
121
+ epoch: EpochNumber(reader.readNumber()),
125
122
  missed: reader.readNumber(),
126
123
  total: reader.readNumber(),
127
124
  });
@@ -139,7 +136,7 @@ export class SentinelStore {
139
136
  const reader = new BufferReader(buffer);
140
137
  const history: ValidatorStatusHistory = [];
141
138
  while (!reader.isEmpty()) {
142
- const slot = BigInt(reader.readNumber());
139
+ const slot = SlotNumber(reader.readNumber());
143
140
  const status = this.statusFromNumber(reader.readUInt8());
144
141
  history.push({ slot, status });
145
142
  }
@@ -148,16 +145,18 @@ export class SentinelStore {
148
145
 
149
146
  private statusToNumber(status: ValidatorStatusInSlot): number {
150
147
  switch (status) {
151
- case 'block-mined':
148
+ case 'checkpoint-mined':
152
149
  return 1;
153
- case 'block-proposed':
150
+ case 'checkpoint-proposed':
154
151
  return 2;
155
- case 'block-missed':
152
+ case 'checkpoint-missed':
156
153
  return 3;
157
154
  case 'attestation-sent':
158
155
  return 4;
159
156
  case 'attestation-missed':
160
157
  return 5;
158
+ case 'blocks-missed':
159
+ return 6;
161
160
  default: {
162
161
  const _exhaustive: never = status;
163
162
  throw new Error(`Unknown status: ${status}`);
@@ -168,15 +167,17 @@ export class SentinelStore {
168
167
  private statusFromNumber(status: number): ValidatorStatusInSlot {
169
168
  switch (status) {
170
169
  case 1:
171
- return 'block-mined';
170
+ return 'checkpoint-mined';
172
171
  case 2:
173
- return 'block-proposed';
172
+ return 'checkpoint-proposed';
174
173
  case 3:
175
- return 'block-missed';
174
+ return 'checkpoint-missed';
176
175
  case 4:
177
176
  return 'attestation-sent';
178
177
  case 5:
179
178
  return 'attestation-missed';
179
+ case 6:
180
+ return 'blocks-missed';
180
181
  default:
181
182
  throw new Error(`Unknown status: ${status}`);
182
183
  }