@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.
- package/dest/aztec-node/config.d.ts +2 -1
- package/dest/aztec-node/config.d.ts.map +1 -1
- package/dest/aztec-node/config.js +2 -0
- package/dest/aztec-node/server.d.ts +5 -16
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +42 -104
- package/dest/sentinel/factory.d.ts +2 -1
- package/dest/sentinel/factory.d.ts.map +1 -1
- package/dest/sentinel/factory.js +1 -1
- package/dest/sentinel/sentinel.d.ts +18 -5
- package/dest/sentinel/sentinel.d.ts.map +1 -1
- package/dest/sentinel/sentinel.js +101 -14
- package/dest/sentinel/store.d.ts +14 -3
- package/dest/sentinel/store.d.ts.map +1 -1
- package/dest/sentinel/store.js +71 -7
- package/package.json +23 -22
- package/src/aztec-node/config.ts +4 -1
- package/src/aztec-node/server.ts +59 -135
- package/src/sentinel/factory.ts +3 -2
- package/src/sentinel/sentinel.ts +114 -6
- package/src/sentinel/store.ts +82 -10
package/src/sentinel/sentinel.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
package/src/sentinel/store.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
11
|
+
public static readonly SCHEMA_VERSION = 2;
|
|
7
12
|
|
|
8
|
-
|
|
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.
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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:
|
|
50
|
-
const data = await this.
|
|
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))]),
|