@aztec/aztec-node 0.87.6 → 1.0.0-nightly.20250604

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.
@@ -5,6 +5,8 @@ import { createLogger } from '@aztec/foundation/log';
5
5
  import { RunningPromise } from '@aztec/foundation/running-promise';
6
6
  import { L2TipsMemoryStore, type L2TipsStore } from '@aztec/kv-store/stores';
7
7
  import type { P2PClient } from '@aztec/p2p';
8
+ import type { SlasherConfig, Watcher, WatcherEmitter } from '@aztec/slasher/config';
9
+ import { Offence, WANT_TO_SLASH_EVENT } from '@aztec/slasher/config';
8
10
  import {
9
11
  type L2BlockSource,
10
12
  L2BlockStream,
@@ -12,18 +14,21 @@ import {
12
14
  type L2BlockStreamEventHandler,
13
15
  getAttestationsFromPublishedL2Block,
14
16
  } from '@aztec/stdlib/block';
15
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
17
+ import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
16
18
  import type {
17
19
  ValidatorStats,
18
20
  ValidatorStatusHistory,
19
21
  ValidatorStatusInSlot,
20
22
  ValidatorStatusType,
23
+ ValidatorsEpochPerformance,
21
24
  ValidatorsStats,
22
25
  } from '@aztec/stdlib/validators';
23
26
 
27
+ import EventEmitter from 'node:events';
28
+
24
29
  import { SentinelStore } from './store.js';
25
30
 
26
- export class Sentinel implements L2BlockStreamEventHandler {
31
+ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) implements L2BlockStreamEventHandler, Watcher {
27
32
  protected runningPromise: RunningPromise;
28
33
  protected blockStream!: L2BlockStream;
29
34
  protected l2TipsStore: L2TipsStore;
@@ -38,8 +43,16 @@ export class Sentinel implements L2BlockStreamEventHandler {
38
43
  protected archiver: L2BlockSource,
39
44
  protected p2p: P2PClient,
40
45
  protected store: SentinelStore,
46
+ protected config: Pick<
47
+ SlasherConfig,
48
+ | 'slashInactivityCreateTargetPercentage'
49
+ | 'slashInactivityCreatePenalty'
50
+ | 'slashInactivitySignalTargetPercentage'
51
+ | 'slashPayloadTtlSeconds'
52
+ >,
41
53
  protected logger = createLogger('node:sentinel'),
42
54
  ) {
55
+ super();
43
56
  this.l2TipsStore = new L2TipsMemoryStore();
44
57
  const interval = (epochCache.getL1Constants().ethereumSlotDuration * 1000) / 4;
45
58
  this.runningPromise = new RunningPromise(this.work.bind(this), logger, interval);
@@ -84,7 +97,96 @@ export class Sentinel implements L2BlockStreamEventHandler {
84
97
  this.slotNumberToBlock.delete(key);
85
98
  }
86
99
  }
100
+ } else if (event.type === 'chain-proven') {
101
+ await this.handleChainProven(event);
102
+ }
103
+ }
104
+
105
+ protected async handleChainProven(event: L2BlockStreamEvent) {
106
+ if (event.type !== 'chain-proven') {
107
+ return;
108
+ }
109
+ const blockNumber = event.block.number;
110
+ const block = await this.archiver.getBlock(blockNumber);
111
+ if (!block) {
112
+ this.logger.error(`Failed to get block ${blockNumber}`, { block });
113
+ return;
87
114
  }
115
+
116
+ const epoch = getEpochAtSlot(block.header.getSlot(), await this.archiver.getL1Constants());
117
+ this.logger.info(`Computing proven performance for epoch ${epoch}`);
118
+ const performance = await this.computeProvenPerformance(epoch);
119
+ this.logger.info(`Proven performance for epoch ${epoch}`, performance);
120
+
121
+ await this.updateProvenPerformance(epoch, performance);
122
+ this.handleProvenPerformance(performance);
123
+ }
124
+
125
+ protected async computeProvenPerformance(epoch: bigint) {
126
+ const headers = await this.archiver.getBlockHeadersForEpoch(epoch);
127
+ const provenSlots = headers.map(h => h.getSlot());
128
+ const fromSlot = provenSlots[0];
129
+ const toSlot = provenSlots[provenSlots.length - 1];
130
+ const { committee } = await this.epochCache.getCommittee(fromSlot);
131
+ const stats = await this.computeStats({ fromSlot, toSlot });
132
+ this.logger.debug(`Stats for epoch ${epoch}`, stats);
133
+
134
+ const performance: ValidatorsEpochPerformance = {};
135
+ for (const validator of Object.keys(stats.stats)) {
136
+ let address;
137
+ try {
138
+ address = EthAddress.fromString(validator);
139
+ } catch (e) {
140
+ this.logger.error(`Invalid validator address ${validator}`, e);
141
+ continue;
142
+ }
143
+ if (!committee.find(v => v.equals(address))) {
144
+ continue;
145
+ }
146
+ let missed = 0;
147
+ for (const history of stats.stats[validator].history) {
148
+ if (provenSlots.includes(history.slot) && history.status === 'attestation-missed') {
149
+ missed++;
150
+ }
151
+ }
152
+ performance[address.toString()] = { missed, total: provenSlots.length };
153
+ }
154
+ return performance;
155
+ }
156
+
157
+ protected updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
158
+ return this.store.updateProvenPerformance(epoch, performance);
159
+ }
160
+
161
+ protected handleProvenPerformance(performance: ValidatorsEpochPerformance) {
162
+ const criminals = Object.entries(performance)
163
+ .filter(([_, { missed, total }]) => {
164
+ return missed / total >= this.config.slashInactivityCreateTargetPercentage;
165
+ })
166
+ .map(([address]) => address as `0x${string}`);
167
+
168
+ const amounts = Array(criminals.length).fill(this.config.slashInactivityCreatePenalty);
169
+ const offenses = Array(criminals.length).fill(Offence.INACTIVITY);
170
+
171
+ this.logger.info(`Criminals: ${criminals.length}`, { criminals, amounts, offenses });
172
+
173
+ if (criminals.length > 0) {
174
+ this.emit(WANT_TO_SLASH_EVENT, { validators: criminals, amounts, offenses });
175
+ }
176
+ }
177
+
178
+ public async shouldSlash(validator: `0x${string}`, _amount: bigint, _offense: Offence): Promise<boolean> {
179
+ const l1Constants = this.epochCache.getL1Constants();
180
+ const ttlL2Slots = this.config.slashPayloadTtlSeconds / l1Constants.slotDuration;
181
+ const ttlEpochs = BigInt(Math.ceil(ttlL2Slots / l1Constants.epochDuration));
182
+
183
+ const currentEpoch = this.epochCache.getEpochAndSlotNow().epoch;
184
+ const performance = await this.store.getProvenPerformance(EthAddress.fromString(validator));
185
+ return (
186
+ performance
187
+ .filter(p => p.epoch >= currentEpoch - ttlEpochs)
188
+ .findIndex(p => p.missed / p.total >= this.config.slashInactivitySignalTargetPercentage) !== -1
189
+ );
88
190
  }
89
191
 
90
192
  /**
@@ -232,14 +334,18 @@ export class Sentinel implements L2BlockStreamEventHandler {
232
334
  }
233
335
 
234
336
  /** Computes stats to be returned based on stored data. */
235
- public async computeStats(): Promise<ValidatorsStats> {
337
+ public async computeStats({
338
+ fromSlot: _fromSlot,
339
+ toSlot: _toSlot,
340
+ }: { fromSlot?: bigint; toSlot?: bigint } = {}): Promise<ValidatorsStats> {
236
341
  const histories = await this.store.getHistories();
237
342
  const slotNow = this.epochCache.getEpochAndSlotNow().slot;
238
- const fromSlot = (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
343
+ const fromSlot = _fromSlot ?? (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
344
+ const toSlot = _toSlot ?? this.lastProcessedSlot ?? slotNow;
239
345
  const result: Record<`0x${string}`, ValidatorStats> = {};
240
346
  for (const [address, history] of Object.entries(histories)) {
241
347
  const validatorAddress = address as `0x${string}`;
242
- result[validatorAddress] = this.computeStatsForValidator(validatorAddress, history, fromSlot);
348
+ result[validatorAddress] = this.computeStatsForValidator(validatorAddress, history, fromSlot, toSlot);
243
349
  }
244
350
  return {
245
351
  stats: result,
@@ -253,8 +359,10 @@ export class Sentinel implements L2BlockStreamEventHandler {
253
359
  address: `0x${string}`,
254
360
  allHistory: ValidatorStatusHistory,
255
361
  fromSlot?: bigint,
362
+ toSlot?: bigint,
256
363
  ): ValidatorStats {
257
- const history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
364
+ let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
365
+ history = toSlot ? history.filter(h => h.slot <= toSlot) : history;
258
366
  return {
259
367
  address: EthAddress.fromString(address),
260
368
  lastProposal: this.computeFromSlot(
@@ -1,56 +1,128 @@
1
+ import { EthAddress } from '@aztec/foundation/eth-address';
1
2
  import { BufferReader, numToUInt8, numToUInt32BE, serializeToBuffer } from '@aztec/foundation/serialize';
2
3
  import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
3
- import type { ValidatorStatusHistory, ValidatorStatusInSlot } from '@aztec/stdlib/validators';
4
+ import type {
5
+ ValidatorStatusHistory,
6
+ ValidatorStatusInSlot,
7
+ ValidatorsEpochPerformance,
8
+ } from '@aztec/stdlib/validators';
4
9
 
5
10
  export class SentinelStore {
6
- public static readonly SCHEMA_VERSION = 1;
11
+ public static readonly SCHEMA_VERSION = 2;
7
12
 
8
- private readonly map: AztecAsyncMap<`0x${string}`, Buffer>;
13
+ // a map from validator address to their ValidatorStatusHistory
14
+ private readonly historyMap: AztecAsyncMap<`0x${string}`, Buffer>;
15
+
16
+ // a map from validator address to their historical proven epoch performance
17
+ // e.g. { validator: [{ epoch: 1, missed: 1, total: 10 }, { epoch: 2, missed: 3, total: 7 }, ...] }
18
+ private readonly provenMap: AztecAsyncMap<`0x${string}`, Buffer>;
9
19
 
10
20
  constructor(
11
21
  private store: AztecAsyncKVStore,
12
22
  private config: { historyLength: number },
13
23
  ) {
14
- this.map = store.openMap('sentinel-validator-status');
24
+ this.historyMap = store.openMap('sentinel-validator-status');
25
+ this.provenMap = store.openMap('sentinel-validator-proven');
15
26
  }
16
27
 
17
28
  public getHistoryLength() {
18
29
  return this.config.historyLength;
19
30
  }
20
31
 
32
+ public async updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
33
+ await this.store.transactionAsync(async () => {
34
+ for (const [who, { missed, total }] of Object.entries(performance)) {
35
+ await this.pushValidatorProvenPerformanceForEpoch({ who: EthAddress.fromString(who), missed, total, epoch });
36
+ }
37
+ });
38
+ }
39
+
40
+ public async getProvenPerformance(who: EthAddress): Promise<{ missed: number; total: number; epoch: bigint }[]> {
41
+ const currentPerformanceBuffer = await this.provenMap.getAsync(who.toString());
42
+ return currentPerformanceBuffer ? this.deserializePerformance(currentPerformanceBuffer) : [];
43
+ }
44
+
45
+ private async pushValidatorProvenPerformanceForEpoch({
46
+ who,
47
+ missed,
48
+ total,
49
+ epoch,
50
+ }: {
51
+ who: EthAddress;
52
+ missed: number;
53
+ total: number;
54
+ epoch: bigint;
55
+ }) {
56
+ const currentPerformance = await this.getProvenPerformance(who);
57
+ const existingIndex = currentPerformance.findIndex(p => p.epoch === epoch);
58
+ if (existingIndex !== -1) {
59
+ currentPerformance[existingIndex] = { missed, total, epoch };
60
+ } else {
61
+ currentPerformance.push({ missed, total, epoch });
62
+ }
63
+
64
+ // This should be sorted by epoch, but just in case.
65
+ // Since we keep the size small, this is not a big deal.
66
+ currentPerformance.sort((a, b) => Number(a.epoch - b.epoch));
67
+
68
+ // keep the most recent `historyLength` entries.
69
+ const performanceToKeep = currentPerformance.slice(-this.config.historyLength);
70
+
71
+ await this.provenMap.set(who.toString(), this.serializePerformance(performanceToKeep));
72
+ }
73
+
21
74
  public async updateValidators(slot: bigint, statuses: Record<`0x${string}`, ValidatorStatusInSlot | undefined>) {
22
75
  await this.store.transactionAsync(async () => {
23
76
  for (const [who, status] of Object.entries(statuses)) {
24
77
  if (status) {
25
- await this.pushValidatorStatusForSlot(who as `0x${string}`, slot, status);
78
+ await this.pushValidatorStatusForSlot(EthAddress.fromString(who), slot, status);
26
79
  }
27
80
  }
28
81
  });
29
82
  }
30
83
 
31
84
  private async pushValidatorStatusForSlot(
32
- who: `0x${string}`,
85
+ who: EthAddress,
33
86
  slot: bigint,
34
87
  status: 'block-mined' | 'block-proposed' | 'block-missed' | 'attestation-sent' | 'attestation-missed',
35
88
  ) {
36
89
  const currentHistory = (await this.getHistory(who)) ?? [];
37
90
  const newHistory = [...currentHistory, { slot, status }].slice(-this.config.historyLength);
38
- await this.map.set(who, this.serializeHistory(newHistory));
91
+ await this.historyMap.set(who.toString(), this.serializeHistory(newHistory));
39
92
  }
40
93
 
41
94
  public async getHistories(): Promise<Record<`0x${string}`, ValidatorStatusHistory>> {
42
95
  const histories: Record<`0x${string}`, ValidatorStatusHistory> = {};
43
- for await (const [address, history] of this.map.entriesAsync()) {
96
+ for await (const [address, history] of this.historyMap.entriesAsync()) {
44
97
  histories[address] = this.deserializeHistory(history);
45
98
  }
46
99
  return histories;
47
100
  }
48
101
 
49
- private async getHistory(address: `0x${string}`): Promise<ValidatorStatusHistory | undefined> {
50
- const data = await this.map.getAsync(address);
102
+ private async getHistory(address: EthAddress): Promise<ValidatorStatusHistory | undefined> {
103
+ const data = await this.historyMap.getAsync(address.toString());
51
104
  return data && this.deserializeHistory(data);
52
105
  }
53
106
 
107
+ private serializePerformance(performance: { missed: number; total: number; epoch: bigint }[]): Buffer {
108
+ return serializeToBuffer(
109
+ performance.map(p => [numToUInt32BE(Number(p.epoch)), numToUInt32BE(p.missed), numToUInt32BE(p.total)]),
110
+ );
111
+ }
112
+
113
+ private deserializePerformance(buffer: Buffer): { missed: number; total: number; epoch: bigint }[] {
114
+ const reader = new BufferReader(buffer);
115
+ const performance: { missed: number; total: number; epoch: bigint }[] = [];
116
+ while (!reader.isEmpty()) {
117
+ performance.push({
118
+ epoch: BigInt(reader.readNumber()),
119
+ missed: reader.readNumber(),
120
+ total: reader.readNumber(),
121
+ });
122
+ }
123
+ return performance;
124
+ }
125
+
54
126
  private serializeHistory(history: ValidatorStatusHistory): Buffer {
55
127
  return serializeToBuffer(
56
128
  history.map(h => [numToUInt32BE(Number(h.slot)), numToUInt8(this.statusToNumber(h.status))]),