@aztec/aztec-node 3.0.0-devnet.6 → 3.0.0-devnet.6-patch.1

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, 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';
@@ -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,7 +91,7 @@ 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
96
  attestors: getAttestationInfoFromPublishedL2Block(block)
95
97
  .filter(a => a.status === 'recovered-from-signature')
@@ -116,7 +118,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
116
118
  if (event.type !== 'chain-proven') {
117
119
  return;
118
120
  }
119
- const blockNumber = event.block.number;
121
+ const blockNumber = BlockNumber(event.block.number);
120
122
  const block = await this.archiver.getBlock(blockNumber);
121
123
  if (!block) {
122
124
  this.logger.error(`Failed to get block ${blockNumber}`, { block });
@@ -134,7 +136,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
134
136
  await this.handleProvenPerformance(epoch, performance);
135
137
  }
136
138
 
137
- protected async computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance> {
139
+ protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
138
140
  const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
139
141
  const { committee } = await this.epochCache.getCommittee(fromSlot);
140
142
  if (!committee) {
@@ -142,7 +144,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
142
144
  return {};
143
145
  }
144
146
 
145
- const stats = await this.computeStats({ fromSlot, toSlot, validators: committee });
147
+ const stats = await this.computeStats({
148
+ fromSlot,
149
+ toSlot,
150
+ validators: committee,
151
+ });
146
152
  this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
147
153
 
148
154
  // Note that we are NOT using the total slots in the epoch as `total` here, since we only
@@ -165,7 +171,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
165
171
  */
166
172
  protected async checkPastInactivity(
167
173
  validator: EthAddress,
168
- currentEpoch: bigint,
174
+ currentEpoch: EpochNumber,
169
175
  requiredConsecutiveEpochs: number,
170
176
  ): Promise<boolean> {
171
177
  if (requiredConsecutiveEpochs === 0) {
@@ -175,23 +181,24 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
175
181
  // Get all historical performance for this validator
176
182
  const allPerformance = await this.store.getProvenPerformance(validator);
177
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
+
178
187
  // If we don't have enough historical data, don't slash
179
- if (allPerformance.length < requiredConsecutiveEpochs) {
188
+ if (pastEpochs.length < requiredConsecutiveEpochs) {
180
189
  this.logger.debug(
181
190
  `Not enough historical data for slashing ${validator} for inactivity (${allPerformance.length} epochs < ${requiredConsecutiveEpochs} required)`,
182
191
  );
183
192
  return false;
184
193
  }
185
194
 
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)
195
+ // Check that we have at least requiredConsecutiveEpochs and that all of them are above the inactivity threshold
196
+ return pastEpochs
190
197
  .slice(0, requiredConsecutiveEpochs)
191
198
  .every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
192
199
  }
193
200
 
194
- protected async handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
201
+ protected async handleProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
195
202
  if (this.config.slashInactivityPenalty === 0n) {
196
203
  return;
197
204
  }
@@ -215,7 +222,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
215
222
  validator: EthAddress.fromString(address),
216
223
  amount: this.config.slashInactivityPenalty,
217
224
  offenseType: OffenseType.INACTIVITY,
218
- epochOrSlot: epoch,
225
+ epochOrSlot: BigInt(epoch),
219
226
  }));
220
227
 
221
228
  if (criminals.length > 0) {
@@ -256,8 +263,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
256
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.
257
264
  * Last, we check the p2p is synced with the archiver, so it has pulled all attestations from it.
258
265
  */
259
- protected async isReadyToProcess(currentSlot: bigint) {
260
- 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);
261
273
  if (this.lastProcessedSlot && this.lastProcessedSlot >= targetSlot) {
262
274
  this.logger.trace(`Already processed slot ${targetSlot}`, { lastProcessedSlot: this.lastProcessedSlot });
263
275
  return false;
@@ -274,7 +286,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
274
286
  }
275
287
 
276
288
  const archiverSlot = await this.archiver.getL2SlotNumber();
277
- if (archiverSlot < targetSlot) {
289
+ if (archiverSlot === undefined || archiverSlot < targetSlot) {
278
290
  this.logger.debug(`Waiting for archiver to sync with L2 slot ${targetSlot}`, { archiverSlot, targetSlot });
279
291
  return false;
280
292
  }
@@ -294,7 +306,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
294
306
  * Gathers committee and proposer data for a given slot, computes slot stats,
295
307
  * and updates overall stats.
296
308
  */
297
- protected async processSlot(slot: bigint) {
309
+ protected async processSlot(slot: SlotNumber) {
298
310
  const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
299
311
  if (!committee || committee.length === 0) {
300
312
  this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
@@ -310,7 +322,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
310
322
  }
311
323
 
312
324
  /** Computes activity for a given slot. */
313
- protected async getSlotActivity(slot: bigint, epoch: bigint, proposer: EthAddress, committee: EthAddress[]) {
325
+ protected async getSlotActivity(slot: SlotNumber, epoch: EpochNumber, proposer: EthAddress, committee: EthAddress[]) {
314
326
  this.logger.debug(`Computing stats for slot ${slot} at epoch ${epoch}`, { slot, epoch, proposer, committee });
315
327
 
316
328
  // Check if there is an L2 block in L1 for this L2 slot
@@ -372,7 +384,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
372
384
  }
373
385
 
374
386
  /** Push the status for each slot for each validator. */
375
- protected updateValidators(slot: bigint, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
387
+ protected updateValidators(slot: SlotNumber, stats: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
376
388
  return this.store.updateValidators(slot, stats);
377
389
  }
378
390
 
@@ -381,13 +393,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
381
393
  fromSlot,
382
394
  toSlot,
383
395
  validators,
384
- }: { fromSlot?: bigint; toSlot?: bigint; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
396
+ }: { fromSlot?: SlotNumber; toSlot?: SlotNumber; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
385
397
  const histories = validators
386
398
  ? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
387
399
  : await this.store.getHistories();
388
400
 
389
401
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
390
- fromSlot ??= (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
402
+ fromSlot ??= SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
391
403
  toSlot ??= this.lastProcessedSlot ?? slotNow;
392
404
 
393
405
  const stats = mapValues(histories, (history, address) =>
@@ -405,8 +417,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
405
417
  /** Computes stats for a single validator. */
406
418
  public async getValidatorStats(
407
419
  validatorAddress: EthAddress,
408
- fromSlot?: bigint,
409
- toSlot?: bigint,
420
+ fromSlot?: SlotNumber,
421
+ toSlot?: SlotNumber,
410
422
  ): Promise<SingleValidatorStats | undefined> {
411
423
  const history = await this.store.getHistory(validatorAddress);
412
424
 
@@ -415,13 +427,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
415
427
  }
416
428
 
417
429
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
418
- 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));
419
432
  const effectiveToSlot = toSlot ?? this.lastProcessedSlot ?? slotNow;
420
433
 
421
434
  const historyLength = BigInt(this.store.getHistoryLength());
422
- if (effectiveToSlot - effectiveFromSlot > historyLength) {
435
+ if (BigInt(effectiveToSlot) - BigInt(effectiveFromSlot) > historyLength) {
423
436
  throw new Error(
424
- `Slot range (${effectiveToSlot - effectiveFromSlot}) exceeds history length (${historyLength}). ` +
437
+ `Slot range (${BigInt(effectiveToSlot) - BigInt(effectiveFromSlot)}) exceeds history length (${historyLength}). ` +
425
438
  `Requested range: ${effectiveFromSlot} to ${effectiveToSlot}.`,
426
439
  );
427
440
  }
@@ -432,11 +445,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
432
445
  effectiveFromSlot,
433
446
  effectiveToSlot,
434
447
  );
435
- const allTimeProvenPerformance = await this.store.getProvenPerformance(validatorAddress);
436
448
 
437
449
  return {
438
450
  validator,
439
- allTimeProvenPerformance,
451
+ allTimeProvenPerformance: await this.store.getProvenPerformance(validatorAddress),
440
452
  lastProcessedSlot: this.lastProcessedSlot,
441
453
  initialSlot: this.initialSlot,
442
454
  slotWindow: this.store.getHistoryLength(),
@@ -446,11 +458,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
446
458
  protected computeStatsForValidator(
447
459
  address: `0x${string}`,
448
460
  allHistory: ValidatorStatusHistory,
449
- fromSlot?: bigint,
450
- toSlot?: bigint,
461
+ fromSlot?: SlotNumber,
462
+ toSlot?: SlotNumber,
451
463
  ): ValidatorStats {
452
- let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
453
- 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;
454
466
  const lastProposal = history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
455
467
  const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
456
468
  return {
@@ -479,7 +491,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
479
491
  };
480
492
  }
481
493
 
482
- protected computeFromSlot(slot: bigint | undefined) {
494
+ protected computeFromSlot(slot: SlotNumber | undefined) {
483
495
  if (slot === undefined) {
484
496
  return undefined;
485
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';
@@ -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
  }