@aztec/aztec-node 4.0.0-nightly.20250907 → 4.0.0-nightly.20260107

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.
@@ -2,6 +2,7 @@ import { type ConfigMappingsType, booleanConfigHelper, numberConfigHelper } from
2
2
 
3
3
  export type SentinelConfig = {
4
4
  sentinelHistoryLengthInEpochs: number;
5
+ sentinelHistoricProvenPerformanceLengthInEpochs: number;
5
6
  sentinelEnabled: boolean;
6
7
  };
7
8
 
@@ -11,6 +12,23 @@ export const sentinelConfigMappings: ConfigMappingsType<SentinelConfig> = {
11
12
  env: 'SENTINEL_HISTORY_LENGTH_IN_EPOCHS',
12
13
  ...numberConfigHelper(24),
13
14
  },
15
+ /**
16
+ * The number of L2 epochs kept of proven performance history for each validator.
17
+ * This value must be large enough so that we have proven performance for every validator
18
+ * for at least slashInactivityConsecutiveEpochThreshold. Assuming this value is 3,
19
+ * and the committee size is 48, and we have 10k validators, then we pick 48 out of 10k each draw.
20
+ * For any fixed element, per-draw prob = 48/10000 = 0.0048.
21
+ * After n draws, count ~ Binomial(n, 0.0048). We want P(X >= 3).
22
+ * Results (exact binomial):
23
+ * - 90% chance: n = 1108
24
+ * - 95% chance: n = 1310
25
+ * - 99% chance: n = 1749
26
+ */
27
+ sentinelHistoricProvenPerformanceLengthInEpochs: {
28
+ description: 'The number of L2 epochs kept of proven performance history for each validator.',
29
+ env: 'SENTINEL_HISTORIC_PROVEN_PERFORMANCE_LENGTH_IN_EPOCHS',
30
+ ...numberConfigHelper(2000),
31
+ },
14
32
  sentinelEnabled: {
15
33
  description: 'Whether the sentinel is enabled or not.',
16
34
  env: 'SENTINEL_ENABLED',
@@ -27,6 +27,10 @@ export async function createSentinel(
27
27
  createLogger('node:sentinel:lmdb'),
28
28
  );
29
29
  const storeHistoryLength = config.sentinelHistoryLengthInEpochs * epochCache.getL1Constants().epochDuration;
30
- const sentinelStore = new SentinelStore(kvStore, { historyLength: storeHistoryLength });
30
+ const storeHistoricProvenPerformanceLength = config.sentinelHistoricProvenPerformanceLengthInEpochs;
31
+ const sentinelStore = new SentinelStore(kvStore, {
32
+ historyLength: storeHistoryLength,
33
+ historicProvenPerformanceLength: storeHistoricProvenPerformanceLength,
34
+ });
31
35
  return new Sentinel(epochCache, archiver, p2p, sentinelStore, config, logger);
32
36
  }
@@ -1,4 +1,5 @@
1
1
  import type { EpochCache } from '@aztec/epoch-cache';
2
+ import { BlockNumber, 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
- getAttestationsFromPublishedL2Block,
22
+ getAttestationInfoFromPublishedL2Block,
22
23
  } from '@aztec/stdlib/block';
23
24
  import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
24
25
  import type {
@@ -40,9 +41,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
40
41
  protected blockStream!: L2BlockStream;
41
42
  protected l2TipsStore: L2TipsStore;
42
43
 
43
- protected initialSlot: bigint | undefined;
44
- protected lastProcessedSlot: bigint | undefined;
45
- protected slotNumberToBlock: Map<bigint, { blockNumber: number; archive: string; attestors: EthAddress[] }> =
44
+ protected initialSlot: SlotNumber | undefined;
45
+ protected lastProcessedSlot: SlotNumber | undefined;
46
+ // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
47
+ protected slotNumberToBlock: Map<SlotNumber, { blockNumber: BlockNumber; archive: string; attestors: EthAddress[] }> =
46
48
  new Map();
47
49
 
48
50
  constructor(
@@ -74,7 +76,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
74
76
  /** Loads initial slot and initializes blockstream. We will not process anything at or before the initial slot. */
75
77
  protected async init() {
76
78
  this.initialSlot = this.epochCache.getEpochAndSlotNow().slot;
77
- const startingBlock = await this.archiver.getBlockNumber();
79
+ const startingBlock = BlockNumber(await this.archiver.getBlockNumber());
78
80
  this.logger.info(`Starting validator sentinel with initial slot ${this.initialSlot} and block ${startingBlock}`);
79
81
  this.blockStream = new L2BlockStream(this.archiver, this.l2TipsStore, this, this.logger, { startingBlock });
80
82
  }
@@ -89,9 +91,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
89
91
  // Store mapping from slot to archive, block number, and attestors
90
92
  for (const block of event.blocks) {
91
93
  this.slotNumberToBlock.set(block.block.header.getSlot(), {
92
- blockNumber: block.block.number,
94
+ blockNumber: BlockNumber(block.block.number),
93
95
  archive: block.block.archive.root.toString(),
94
- attestors: getAttestationsFromPublishedL2Block(block).map(att => att.getSender()),
96
+ attestors: getAttestationInfoFromPublishedL2Block(block)
97
+ .filter(a => a.status === 'recovered-from-signature')
98
+ .map(a => a.address!),
95
99
  });
96
100
  }
97
101
 
@@ -114,7 +118,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
114
118
  if (event.type !== 'chain-proven') {
115
119
  return;
116
120
  }
117
- const blockNumber = event.block.number;
121
+ const blockNumber = BlockNumber(event.block.number);
118
122
  const block = await this.archiver.getBlock(blockNumber);
119
123
  if (!block) {
120
124
  this.logger.error(`Failed to get block ${blockNumber}`, { block });
@@ -132,7 +136,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
132
136
  await this.handleProvenPerformance(epoch, performance);
133
137
  }
134
138
 
135
- protected async computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance> {
139
+ protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
136
140
  const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
137
141
  const { committee } = await this.epochCache.getCommittee(fromSlot);
138
142
  if (!committee) {
@@ -140,7 +144,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
140
144
  return {};
141
145
  }
142
146
 
143
- const stats = await this.computeStats({ fromSlot, toSlot, validators: committee });
147
+ const stats = await this.computeStats({
148
+ fromSlot,
149
+ toSlot,
150
+ validators: committee,
151
+ });
144
152
  this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
145
153
 
146
154
  // Note that we are NOT using the total slots in the epoch as `total` here, since we only
@@ -163,7 +171,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
163
171
  */
164
172
  protected async checkPastInactivity(
165
173
  validator: EthAddress,
166
- currentEpoch: bigint,
174
+ currentEpoch: EpochNumber,
167
175
  requiredConsecutiveEpochs: number,
168
176
  ): Promise<boolean> {
169
177
  if (requiredConsecutiveEpochs === 0) {
@@ -173,23 +181,28 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
173
181
  // Get all historical performance for this validator
174
182
  const allPerformance = await this.store.getProvenPerformance(validator);
175
183
 
184
+ // Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
185
+ const pastEpochs = allPerformance.sort((a, b) => Number(b.epoch - a.epoch)).filter(p => p.epoch < currentEpoch);
186
+
176
187
  // If we don't have enough historical data, don't slash
177
- if (allPerformance.length < requiredConsecutiveEpochs) {
188
+ if (pastEpochs.length < requiredConsecutiveEpochs) {
178
189
  this.logger.debug(
179
190
  `Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
180
191
  );
181
192
  return false;
182
193
  }
183
194
 
184
- // Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
185
- return allPerformance
186
- .sort((a, b) => Number(b.epoch - a.epoch))
187
- .filter(p => p.epoch < currentEpoch)
195
+ // Check that we have at least requiredConsecutiveEpochs and that all of them are above the inactivity threshold
196
+ return pastEpochs
188
197
  .slice(0, requiredConsecutiveEpochs)
189
198
  .every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
190
199
  }
191
200
 
192
- protected async handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
201
+ protected async handleProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
202
+ if (this.config.slashInactivityPenalty === 0n) {
203
+ return;
204
+ }
205
+
193
206
  const inactiveValidators = getEntries(performance)
194
207
  .filter(([_, { missed, total }]) => missed / total >= this.config.slashInactivityTargetPercentage)
195
208
  .map(([address]) => address);
@@ -209,11 +222,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
209
222
  validator: EthAddress.fromString(address),
210
223
  amount: this.config.slashInactivityPenalty,
211
224
  offenseType: OffenseType.INACTIVITY,
212
- epochOrSlot: epoch,
225
+ epochOrSlot: BigInt(epoch),
213
226
  }));
214
227
 
215
228
  if (criminals.length > 0) {
216
- this.logger.info(
229
+ this.logger.verbose(
217
230
  `Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`,
218
231
  { ...args, epochThreshold },
219
232
  );
@@ -250,8 +263,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
250
263
  * 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.
251
264
  * Last, we check the p2p is synced with the archiver, so it has pulled all attestations from it.
252
265
  */
253
- protected async isReadyToProcess(currentSlot: bigint) {
254
- const targetSlot = currentSlot - 2n;
266
+ protected async isReadyToProcess(currentSlot: SlotNumber): Promise<SlotNumber | false> {
267
+ if (currentSlot < 2) {
268
+ this.logger.trace(`Current slot ${currentSlot} too early.`);
269
+ return false;
270
+ }
271
+
272
+ const targetSlot = SlotNumber(currentSlot - 2);
255
273
  if (this.lastProcessedSlot && this.lastProcessedSlot >= targetSlot) {
256
274
  this.logger.trace(`Already processed slot ${targetSlot}`, { lastProcessedSlot: this.lastProcessedSlot });
257
275
  return false;
@@ -268,7 +286,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
268
286
  }
269
287
 
270
288
  const archiverSlot = await this.archiver.getL2SlotNumber();
271
- if (archiverSlot < targetSlot) {
289
+ if (archiverSlot === undefined || archiverSlot < targetSlot) {
272
290
  this.logger.debug(`Waiting for archiver to sync with L2 slot ${targetSlot}`, { archiverSlot, targetSlot });
273
291
  return false;
274
292
  }
@@ -288,7 +306,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
288
306
  * Gathers committee and proposer data for a given slot, computes slot stats,
289
307
  * and updates overall stats.
290
308
  */
291
- protected async processSlot(slot: bigint) {
309
+ protected async processSlot(slot: SlotNumber) {
292
310
  const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
293
311
  if (!committee || committee.length === 0) {
294
312
  this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
@@ -304,7 +322,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
304
322
  }
305
323
 
306
324
  /** Computes activity for a given slot. */
307
- protected async getSlotActivity(slot: bigint, epoch: bigint, proposer: EthAddress, committee: EthAddress[]) {
325
+ protected async getSlotActivity(slot: SlotNumber, epoch: EpochNumber, proposer: EthAddress, committee: EthAddress[]) {
308
326
  this.logger.debug(`Computing stats for slot ${slot} at epoch ${epoch}`, { slot, epoch, proposer, committee });
309
327
 
310
328
  // Check if there is an L2 block in L1 for this L2 slot
@@ -315,8 +333,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
315
333
  // (contains the ones synced from mined blocks, which we may have missed from p2p).
316
334
  const block = this.slotNumberToBlock.get(slot);
317
335
  const p2pAttested = await this.p2p.getAttestationsForSlot(slot, block?.archive);
336
+ // Filter out attestations with invalid signatures
337
+ const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
318
338
  const attestors = new Set(
319
- [...p2pAttested.map(a => a.getSender().toString()), ...(block?.attestors.map(a => a.toString()) ?? [])].filter(
339
+ [...p2pAttestors.map(a => a.toString()), ...(block?.attestors.map(a => a.toString()) ?? [])].filter(
320
340
  addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
321
341
  ),
322
342
  );
@@ -364,7 +384,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
364
384
  }
365
385
 
366
386
  /** Push the status for each slot for each validator. */
367
- protected updateValidators(slot: bigint, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
387
+ protected updateValidators(slot: SlotNumber, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
368
388
  return this.store.updateValidators(slot, stats);
369
389
  }
370
390
 
@@ -373,13 +393,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
373
393
  fromSlot,
374
394
  toSlot,
375
395
  validators,
376
- }: { fromSlot?: bigint; toSlot?: bigint; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
396
+ }: { fromSlot?: SlotNumber; toSlot?: SlotNumber; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
377
397
  const histories = validators
378
398
  ? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
379
399
  : await this.store.getHistories();
380
400
 
381
401
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
382
- fromSlot ??= (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
402
+ fromSlot ??= SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
383
403
  toSlot ??= this.lastProcessedSlot ?? slotNow;
384
404
 
385
405
  const stats = mapValues(histories, (history, address) =>
@@ -397,8 +417,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
397
417
  /** Computes stats for a single validator. */
398
418
  public async getValidatorStats(
399
419
  validatorAddress: EthAddress,
400
- fromSlot?: bigint,
401
- toSlot?: bigint,
420
+ fromSlot?: SlotNumber,
421
+ toSlot?: SlotNumber,
402
422
  ): Promise<SingleValidatorStats | undefined> {
403
423
  const history = await this.store.getHistory(validatorAddress);
404
424
 
@@ -407,13 +427,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
407
427
  }
408
428
 
409
429
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
410
- const effectiveFromSlot = fromSlot ?? (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
430
+ const effectiveFromSlot =
431
+ fromSlot ?? SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
411
432
  const effectiveToSlot = toSlot ?? this.lastProcessedSlot ?? slotNow;
412
433
 
413
434
  const historyLength = BigInt(this.store.getHistoryLength());
414
- if (effectiveToSlot - effectiveFromSlot > historyLength) {
435
+ if (BigInt(effectiveToSlot) - BigInt(effectiveFromSlot) > historyLength) {
415
436
  throw new Error(
416
- `Slot range (${effectiveToSlot - effectiveFromSlot}) exceeds history length (${historyLength}). ` +
437
+ `Slot range (${BigInt(effectiveToSlot) - BigInt(effectiveFromSlot)}) exceeds history length (${historyLength}). ` +
417
438
  `Requested range: ${effectiveFromSlot} to ${effectiveToSlot}.`,
418
439
  );
419
440
  }
@@ -424,11 +445,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
424
445
  effectiveFromSlot,
425
446
  effectiveToSlot,
426
447
  );
427
- const allTimeProvenPerformance = await this.store.getProvenPerformance(validatorAddress);
428
448
 
429
449
  return {
430
450
  validator,
431
- allTimeProvenPerformance,
451
+ allTimeProvenPerformance: await this.store.getProvenPerformance(validatorAddress),
432
452
  lastProcessedSlot: this.lastProcessedSlot,
433
453
  initialSlot: this.initialSlot,
434
454
  slotWindow: this.store.getHistoryLength(),
@@ -438,11 +458,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
438
458
  protected computeStatsForValidator(
439
459
  address: `0x${string}`,
440
460
  allHistory: ValidatorStatusHistory,
441
- fromSlot?: bigint,
442
- toSlot?: bigint,
461
+ fromSlot?: SlotNumber,
462
+ toSlot?: SlotNumber,
443
463
  ): ValidatorStats {
444
- let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
445
- history = toSlot ? history.filter(h => h.slot <= toSlot) : history;
464
+ let history = fromSlot ? allHistory.filter(h => BigInt(h.slot) >= fromSlot) : allHistory;
465
+ history = toSlot ? history.filter(h => BigInt(h.slot) <= toSlot) : history;
446
466
  const lastProposal = history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
447
467
  const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
448
468
  return {
@@ -471,7 +491,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
471
491
  };
472
492
  }
473
493
 
474
- protected computeFromSlot(slot: bigint | undefined) {
494
+ protected computeFromSlot(slot: SlotNumber | undefined) {
475
495
  if (slot === undefined) {
476
496
  return undefined;
477
497
  }
@@ -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';
@@ -19,7 +20,7 @@ export class SentinelStore {
19
20
 
20
21
  constructor(
21
22
  private store: AztecAsyncKVStore,
22
- private config: { historyLength: number },
23
+ private config: { historyLength: number; historicProvenPerformanceLength: number },
23
24
  ) {
24
25
  this.historyMap = store.openMap('sentinel-validator-status');
25
26
  this.provenMap = store.openMap('sentinel-validator-proven');
@@ -29,7 +30,11 @@ export class SentinelStore {
29
30
  return this.config.historyLength;
30
31
  }
31
32
 
32
- public async updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
33
+ public getHistoricProvenPerformanceLength() {
34
+ return this.config.historicProvenPerformanceLength;
35
+ }
36
+
37
+ public async updateProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
33
38
  await this.store.transactionAsync(async () => {
34
39
  for (const [who, { missed, total }] of Object.entries(performance)) {
35
40
  await this.pushValidatorProvenPerformanceForEpoch({ who: EthAddress.fromString(who), missed, total, epoch });
@@ -37,7 +42,7 @@ export class SentinelStore {
37
42
  });
38
43
  }
39
44
 
40
- 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 }[]> {
41
46
  const currentPerformanceBuffer = await this.provenMap.getAsync(who.toString());
42
47
  return currentPerformanceBuffer ? this.deserializePerformance(currentPerformanceBuffer) : [];
43
48
  }
@@ -51,7 +56,7 @@ export class SentinelStore {
51
56
  who: EthAddress;
52
57
  missed: number;
53
58
  total: number;
54
- epoch: bigint;
59
+ epoch: EpochNumber;
55
60
  }) {
56
61
  const currentPerformance = await this.getProvenPerformance(who);
57
62
  const existingIndex = currentPerformance.findIndex(p => p.epoch === epoch);
@@ -65,13 +70,13 @@ export class SentinelStore {
65
70
  // Since we keep the size small, this is not a big deal.
66
71
  currentPerformance.sort((a, b) => Number(a.epoch - b.epoch));
67
72
 
68
- // keep the most recent `historyLength` entries.
69
- const performanceToKeep = currentPerformance.slice(-this.config.historyLength);
73
+ // keep the most recent `historicProvenPerformanceLength` entries.
74
+ const performanceToKeep = currentPerformance.slice(-this.config.historicProvenPerformanceLength);
70
75
 
71
76
  await this.provenMap.set(who.toString(), this.serializePerformance(performanceToKeep));
72
77
  }
73
78
 
74
- public async updateValidators(slot: bigint, statuses: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
79
+ public async updateValidators(slot: SlotNumber, statuses: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
75
80
  await this.store.transactionAsync(async () => {
76
81
  for (const [who, status] of Object.entries(statuses)) {
77
82
  if (status) {
@@ -83,7 +88,7 @@ export class SentinelStore {
83
88
 
84
89
  private async pushValidatorStatusForSlot(
85
90
  who: EthAddress,
86
- slot: bigint,
91
+ slot: SlotNumber,
87
92
  status: 'block-mined' | 'block-proposed' | 'block-missed' | 'attestation-sent' | 'attestation-missed',
88
93
  ) {
89
94
  await this.store.transactionAsync(async () => {
@@ -106,18 +111,18 @@ export class SentinelStore {
106
111
  return data && this.deserializeHistory(data);
107
112
  }
108
113
 
109
- private serializePerformance(performance: { missed: number; total: number; epoch: bigint }[]): Buffer {
114
+ private serializePerformance(performance: { missed: number; total: number; epoch: EpochNumber }[]): Buffer {
110
115
  return serializeToBuffer(
111
116
  performance.map(p => [numToUInt32BE(Number(p.epoch)), numToUInt32BE(p.missed), numToUInt32BE(p.total)]),
112
117
  );
113
118
  }
114
119
 
115
- private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: bigint }[] {
120
+ private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: EpochNumber }[] {
116
121
  const reader = new BufferReader(buffer);
117
- const performance: { missed: number; total: number; epoch: bigint }[] = [];
122
+ const performance: { missed: number; total: number; epoch: EpochNumber }[] = [];
118
123
  while (!reader.isEmpty()) {
119
124
  performance.push({
120
- epoch: BigInt(reader.readNumber()),
125
+ epoch: EpochNumber(reader.readNumber()),
121
126
  missed: reader.readNumber(),
122
127
  total: reader.readNumber(),
123
128
  });
@@ -135,7 +140,7 @@ export class SentinelStore {
135
140
  const reader = new BufferReader(buffer);
136
141
  const history: ValidatorStatusHistory = [];
137
142
  while (!reader.isEmpty()) {
138
- const slot = BigInt(reader.readNumber());
143
+ const slot = SlotNumber(reader.readNumber());
139
144
  const status = this.statusFromNumber(reader.readUInt8());
140
145
  history.push({ slot, status });
141
146
  }