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

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.
@@ -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 {
@@ -40,10 +41,13 @@ 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[] }> =
46
- new Map();
44
+ protected initialSlot: SlotNumber | undefined;
45
+ protected lastProcessedSlot: SlotNumber | undefined;
46
+ // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
47
+ protected slotNumberToCheckpoint: Map<
48
+ SlotNumber,
49
+ { checkpointNumber: CheckpointNumber; archive: string; attestors: EthAddress[] }
50
+ > = new Map();
47
51
 
48
52
  constructor(
49
53
  protected epochCache: EpochCache,
@@ -74,7 +78,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
74
78
  /** Loads initial slot and initializes blockstream. We will not process anything at or before the initial slot. */
75
79
  protected async init() {
76
80
  this.initialSlot = this.epochCache.getEpochAndSlotNow().slot;
77
- const startingBlock = await this.archiver.getBlockNumber();
81
+ const startingBlock = BlockNumber(await this.archiver.getBlockNumber());
78
82
  this.logger.info(`Starting validator sentinel with initial slot ${this.initialSlot} and block ${startingBlock}`);
79
83
  this.blockStream = new L2BlockStream(this.archiver, this.l2TipsStore, this, this.logger, { startingBlock });
80
84
  }
@@ -85,39 +89,46 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
85
89
 
86
90
  public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
87
91
  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
- }
92
+ if (event.type === 'chain-checkpointed') {
93
+ this.handleCheckpoint(event);
110
94
  } else if (event.type === 'chain-proven') {
111
95
  await this.handleChainProven(event);
112
96
  }
113
97
  }
114
98
 
99
+ protected handleCheckpoint(event: L2BlockStreamEvent) {
100
+ if (event.type !== 'chain-checkpointed') {
101
+ return;
102
+ }
103
+ const checkpoint = event.checkpoint;
104
+
105
+ // Store mapping from slot to archive, checkpoint number, and attestors
106
+ this.slotNumberToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, {
107
+ checkpointNumber: checkpoint.checkpoint.number,
108
+ archive: checkpoint.checkpoint.archive.root.toString(),
109
+ attestors: getAttestationInfoFromPublishedCheckpoint(checkpoint)
110
+ .filter(a => a.status === 'recovered-from-signature')
111
+ .map(a => a.address!),
112
+ });
113
+
114
+ // Prune the archive map to only keep at most N entries
115
+ const historyLength = this.store.getHistoryLength();
116
+ if (this.slotNumberToCheckpoint.size > historyLength) {
117
+ const toDelete = Array.from(this.slotNumberToCheckpoint.keys())
118
+ .sort((a, b) => Number(a - b))
119
+ .slice(0, this.slotNumberToCheckpoint.size - historyLength);
120
+ for (const key of toDelete) {
121
+ this.slotNumberToCheckpoint.delete(key);
122
+ }
123
+ }
124
+ }
125
+
115
126
  protected async handleChainProven(event: L2BlockStreamEvent) {
116
127
  if (event.type !== 'chain-proven') {
117
128
  return;
118
129
  }
119
130
  const blockNumber = event.block.number;
120
- const block = await this.archiver.getBlock(blockNumber);
131
+ const block = await this.archiver.getL2Block(blockNumber);
121
132
  if (!block) {
122
133
  this.logger.error(`Failed to get block ${blockNumber}`, { block });
123
134
  return;
@@ -134,7 +145,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
134
145
  await this.handleProvenPerformance(epoch, performance);
135
146
  }
136
147
 
137
- protected async computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance> {
148
+ protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
138
149
  const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
139
150
  const { committee } = await this.epochCache.getCommittee(fromSlot);
140
151
  if (!committee) {
@@ -142,7 +153,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
142
153
  return {};
143
154
  }
144
155
 
145
- const stats = await this.computeStats({ fromSlot, toSlot, validators: committee });
156
+ const stats = await this.computeStats({
157
+ fromSlot,
158
+ toSlot,
159
+ validators: committee,
160
+ });
146
161
  this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
147
162
 
148
163
  // Note that we are NOT using the total slots in the epoch as `total` here, since we only
@@ -165,7 +180,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
165
180
  */
166
181
  protected async checkPastInactivity(
167
182
  validator: EthAddress,
168
- currentEpoch: bigint,
183
+ currentEpoch: EpochNumber,
169
184
  requiredConsecutiveEpochs: number,
170
185
  ): Promise<boolean> {
171
186
  if (requiredConsecutiveEpochs === 0) {
@@ -175,23 +190,24 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
175
190
  // Get all historical performance for this validator
176
191
  const allPerformance = await this.store.getProvenPerformance(validator);
177
192
 
193
+ // Sort by epoch descending to get most recent first, keep only epochs strictly before the current one, and get the first N
194
+ const pastEpochs = allPerformance.sort((a, b) => Number(b.epoch - a.epoch)).filter(p => p.epoch < currentEpoch);
195
+
178
196
  // If we don't have enough historical data, don't slash
179
- if (allPerformance.length < requiredConsecutiveEpochs) {
197
+ if (pastEpochs.length < requiredConsecutiveEpochs) {
180
198
  this.logger.debug(
181
199
  `Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
182
200
  );
183
201
  return false;
184
202
  }
185
203
 
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)
204
+ // Check that we have at least requiredConsecutiveEpochs and that all of them are above the inactivity threshold
205
+ return pastEpochs
190
206
  .slice(0, requiredConsecutiveEpochs)
191
207
  .every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
192
208
  }
193
209
 
194
- protected async handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
210
+ protected async handleProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
195
211
  if (this.config.slashInactivityPenalty === 0n) {
196
212
  return;
197
213
  }
@@ -215,7 +231,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
215
231
  validator: EthAddress.fromString(address),
216
232
  amount: this.config.slashInactivityPenalty,
217
233
  offenseType: OffenseType.INACTIVITY,
218
- epochOrSlot: epoch,
234
+ epochOrSlot: BigInt(epoch),
219
235
  }));
220
236
 
221
237
  if (criminals.length > 0) {
@@ -256,8 +272,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
256
272
  * 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
273
  * Last, we check the p2p is synced with the archiver, so it has pulled all attestations from it.
258
274
  */
259
- protected async isReadyToProcess(currentSlot: bigint) {
260
- const targetSlot = currentSlot - 2n;
275
+ protected async isReadyToProcess(currentSlot: SlotNumber): Promise<SlotNumber | false> {
276
+ if (currentSlot < 2) {
277
+ this.logger.trace(`Current slot ${currentSlot} too early.`);
278
+ return false;
279
+ }
280
+
281
+ const targetSlot = SlotNumber(currentSlot - 2);
261
282
  if (this.lastProcessedSlot && this.lastProcessedSlot >= targetSlot) {
262
283
  this.logger.trace(`Already processed slot ${targetSlot}`, { lastProcessedSlot: this.lastProcessedSlot });
263
284
  return false;
@@ -279,8 +300,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
279
300
  return false;
280
301
  }
281
302
 
282
- const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.latest.hash);
283
- const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.latest.hash);
303
+ const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.proposed.hash);
304
+ const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.proposed.hash);
284
305
  const isP2pSynced = archiverLastBlockHash === p2pLastBlockHash;
285
306
  if (!isP2pSynced) {
286
307
  this.logger.debug(`Waiting for P2P client to sync with archiver`, { archiverLastBlockHash, p2pLastBlockHash });
@@ -294,7 +315,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
294
315
  * Gathers committee and proposer data for a given slot, computes slot stats,
295
316
  * and updates overall stats.
296
317
  */
297
- protected async processSlot(slot: bigint) {
318
+ protected async processSlot(slot: SlotNumber) {
298
319
  const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
299
320
  if (!committee || committee.length === 0) {
300
321
  this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
@@ -310,7 +331,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
310
331
  }
311
332
 
312
333
  /** Computes activity for a given slot. */
313
- protected async getSlotActivity(slot: bigint, epoch: bigint, proposer: EthAddress, committee: EthAddress[]) {
334
+ protected async getSlotActivity(slot: SlotNumber, epoch: EpochNumber, proposer: EthAddress, committee: EthAddress[]) {
314
335
  this.logger.debug(`Computing stats for slot ${slot} at epoch ${epoch}`, { slot, epoch, proposer, committee });
315
336
 
316
337
  // Check if there is an L2 block in L1 for this L2 slot
@@ -319,8 +340,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
319
340
  // or all attestations for all proposals in the slot if no block was mined.
320
341
  // We gather from both p2p (contains the ones seen on the p2p layer) and archiver
321
342
  // (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);
343
+ const block = this.slotNumberToCheckpoint.get(slot);
344
+ const p2pAttested = await this.p2p.getCheckpointAttestationsForSlot(slot, block?.archive);
324
345
  // Filter out attestations with invalid signatures
325
346
  const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
326
347
  const attestors = new Set(
@@ -372,7 +393,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
372
393
  }
373
394
 
374
395
  /** Push the status for each slot for each validator. */
375
- protected updateValidators(slot: bigint, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
396
+ protected updateValidators(slot: SlotNumber, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
376
397
  return this.store.updateValidators(slot, stats);
377
398
  }
378
399
 
@@ -381,13 +402,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
381
402
  fromSlot,
382
403
  toSlot,
383
404
  validators,
384
- }: { fromSlot?: bigint; toSlot?: bigint; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
405
+ }: { fromSlot?: SlotNumber; toSlot?: SlotNumber; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
385
406
  const histories = validators
386
407
  ? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
387
408
  : await this.store.getHistories();
388
409
 
389
410
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
390
- fromSlot ??= (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
411
+ fromSlot ??= SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
391
412
  toSlot ??= this.lastProcessedSlot ?? slotNow;
392
413
 
393
414
  const stats = mapValues(histories, (history, address) =>
@@ -405,8 +426,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
405
426
  /** Computes stats for a single validator. */
406
427
  public async getValidatorStats(
407
428
  validatorAddress: EthAddress,
408
- fromSlot?: bigint,
409
- toSlot?: bigint,
429
+ fromSlot?: SlotNumber,
430
+ toSlot?: SlotNumber,
410
431
  ): Promise<SingleValidatorStats | undefined> {
411
432
  const history = await this.store.getHistory(validatorAddress);
412
433
 
@@ -415,13 +436,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
415
436
  }
416
437
 
417
438
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
418
- const effectiveFromSlot = fromSlot ?? (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
439
+ const effectiveFromSlot =
440
+ fromSlot ?? SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
419
441
  const effectiveToSlot = toSlot ?? this.lastProcessedSlot ?? slotNow;
420
442
 
421
443
  const historyLength = BigInt(this.store.getHistoryLength());
422
- if (effectiveToSlot - effectiveFromSlot > historyLength) {
444
+ if (BigInt(effectiveToSlot) - BigInt(effectiveFromSlot) > historyLength) {
423
445
  throw new Error(
424
- `Slot range (${effectiveToSlot - effectiveFromSlot}) exceeds history length (${historyLength}). ` +
446
+ `Slot range (${BigInt(effectiveToSlot) - BigInt(effectiveFromSlot)}) exceeds history length (${historyLength}). ` +
425
447
  `Requested range: ${effectiveFromSlot} to ${effectiveToSlot}.`,
426
448
  );
427
449
  }
@@ -432,11 +454,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
432
454
  effectiveFromSlot,
433
455
  effectiveToSlot,
434
456
  );
435
- const allTimeProvenPerformance = await this.store.getProvenPerformance(validatorAddress);
436
457
 
437
458
  return {
438
459
  validator,
439
- allTimeProvenPerformance,
460
+ allTimeProvenPerformance: await this.store.getProvenPerformance(validatorAddress),
440
461
  lastProcessedSlot: this.lastProcessedSlot,
441
462
  initialSlot: this.initialSlot,
442
463
  slotWindow: this.store.getHistoryLength(),
@@ -446,11 +467,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
446
467
  protected computeStatsForValidator(
447
468
  address: `0x${string}`,
448
469
  allHistory: ValidatorStatusHistory,
449
- fromSlot?: bigint,
450
- toSlot?: bigint,
470
+ fromSlot?: SlotNumber,
471
+ toSlot?: SlotNumber,
451
472
  ): ValidatorStats {
452
- let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
453
- history = toSlot ? history.filter(h => h.slot <= toSlot) : history;
473
+ let history = fromSlot ? allHistory.filter(h => BigInt(h.slot) >= fromSlot) : allHistory;
474
+ history = toSlot ? history.filter(h => BigInt(h.slot) <= toSlot) : history;
454
475
  const lastProposal = history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
455
476
  const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
456
477
  return {
@@ -479,7 +500,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
479
500
  };
480
501
  }
481
502
 
482
- protected computeFromSlot(slot: bigint | undefined) {
503
+ protected computeFromSlot(slot: SlotNumber | undefined) {
483
504
  if (slot === undefined) {
484
505
  return undefined;
485
506
  }
@@ -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';
@@ -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) {
@@ -87,7 +88,7 @@ export class SentinelStore {
87
88
 
88
89
  private async pushValidatorStatusForSlot(
89
90
  who: EthAddress,
90
- slot: bigint,
91
+ slot: SlotNumber,
91
92
  status: 'block-mined' | 'block-proposed' | 'block-missed' | 'attestation-sent' | 'attestation-missed',
92
93
  ) {
93
94
  await this.store.transactionAsync(async () => {
@@ -110,18 +111,18 @@ export class SentinelStore {
110
111
  return data && this.deserializeHistory(data);
111
112
  }
112
113
 
113
- private serializePerformance(performance: { missed: number; total: number; epoch: bigint }[]): Buffer {
114
+ private serializePerformance(performance: { missed: number; total: number; epoch: EpochNumber }[]): Buffer {
114
115
  return serializeToBuffer(
115
116
  performance.map(p => [numToUInt32BE(Number(p.epoch)), numToUInt32BE(p.missed), numToUInt32BE(p.total)]),
116
117
  );
117
118
  }
118
119
 
119
- private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: bigint }[] {
120
+ private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: EpochNumber }[] {
120
121
  const reader = new BufferReader(buffer);
121
- const performance: { missed: number; total: number; epoch: bigint }[] = [];
122
+ const performance: { missed: number; total: number; epoch: EpochNumber }[] = [];
122
123
  while (!reader.isEmpty()) {
123
124
  performance.push({
124
- epoch: BigInt(reader.readNumber()),
125
+ epoch: EpochNumber(reader.readNumber()),
125
126
  missed: reader.readNumber(),
126
127
  total: reader.readNumber(),
127
128
  });
@@ -139,7 +140,7 @@ export class SentinelStore {
139
140
  const reader = new BufferReader(buffer);
140
141
  const history: ValidatorStatusHistory = [];
141
142
  while (!reader.isEmpty()) {
142
- const slot = BigInt(reader.readNumber());
143
+ const slot = SlotNumber(reader.readNumber());
143
144
  const status = this.statusFromNumber(reader.readUInt8());
144
145
  history.push({ slot, status });
145
146
  }