@aztec/aztec-node 0.0.1-commit.5daedc8 → 0.0.1-commit.6201a7b05
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/block_response_helpers.d.ts +21 -0
- package/dest/aztec-node/block_response_helpers.d.ts.map +1 -0
- package/dest/aztec-node/block_response_helpers.js +88 -0
- package/dest/aztec-node/config.d.ts +12 -6
- package/dest/aztec-node/config.d.ts.map +1 -1
- package/dest/aztec-node/config.js +18 -4
- package/dest/aztec-node/node_metrics.d.ts +1 -1
- package/dest/aztec-node/node_metrics.d.ts.map +1 -1
- package/dest/aztec-node/node_metrics.js +9 -16
- package/dest/aztec-node/server.d.ts +87 -178
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +1338 -431
- package/dest/sentinel/factory.d.ts +4 -3
- package/dest/sentinel/factory.d.ts.map +1 -1
- package/dest/sentinel/factory.js +1 -1
- package/dest/sentinel/sentinel.d.ts +11 -7
- package/dest/sentinel/sentinel.d.ts.map +1 -1
- package/dest/sentinel/sentinel.js +97 -60
- package/dest/sentinel/store.d.ts +2 -2
- package/dest/sentinel/store.d.ts.map +1 -1
- package/dest/sentinel/store.js +11 -7
- package/package.json +29 -28
- package/src/aztec-node/block_response_helpers.ts +131 -0
- package/src/aztec-node/config.ts +35 -15
- package/src/aztec-node/node_metrics.ts +6 -17
- package/src/aztec-node/server.ts +1153 -566
- package/src/sentinel/factory.ts +4 -8
- package/src/sentinel/sentinel.ts +119 -65
- package/src/sentinel/store.ts +12 -12
package/src/sentinel/factory.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
2
|
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
-
import type { DataStoreConfig } from '@aztec/kv-store/config';
|
|
4
3
|
import { createStore } from '@aztec/kv-store/lmdb-v2';
|
|
5
4
|
import type { P2PClient } from '@aztec/p2p';
|
|
6
5
|
import type { L2BlockSource } from '@aztec/stdlib/block';
|
|
6
|
+
import type { ChainConfig } from '@aztec/stdlib/config';
|
|
7
7
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
8
|
+
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
|
|
8
9
|
|
|
9
10
|
import type { SentinelConfig } from './config.js';
|
|
10
11
|
import { Sentinel } from './sentinel.js';
|
|
@@ -14,18 +15,13 @@ export async function createSentinel(
|
|
|
14
15
|
epochCache: EpochCache,
|
|
15
16
|
archiver: L2BlockSource,
|
|
16
17
|
p2p: P2PClient,
|
|
17
|
-
config: SentinelConfig & DataStoreConfig & SlasherConfig,
|
|
18
|
+
config: SentinelConfig & DataStoreConfig & SlasherConfig & Pick<ChainConfig, 'l1ChainId' | 'l1Contracts'>,
|
|
18
19
|
logger = createLogger('node:sentinel'),
|
|
19
20
|
): Promise<Sentinel | undefined> {
|
|
20
21
|
if (!config.sentinelEnabled) {
|
|
21
22
|
return undefined;
|
|
22
23
|
}
|
|
23
|
-
const kvStore = await createStore(
|
|
24
|
-
'sentinel',
|
|
25
|
-
SentinelStore.SCHEMA_VERSION,
|
|
26
|
-
config,
|
|
27
|
-
createLogger('node:sentinel:lmdb'),
|
|
28
|
-
);
|
|
24
|
+
const kvStore = await createStore('sentinel', SentinelStore.SCHEMA_VERSION, config, logger.getBindings());
|
|
29
25
|
const storeHistoryLength = config.sentinelHistoryLengthInEpochs * epochCache.getL1Constants().epochDuration;
|
|
30
26
|
const storeHistoricProvenPerformanceLength = config.sentinelHistoricProvenPerformanceLengthInEpochs;
|
|
31
27
|
const sentinelStore = new SentinelStore(kvStore, {
|
package/src/sentinel/sentinel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
3
|
import { countWhile, filterAsync, fromEntries, getEntries, mapValues } from '@aztec/foundation/collection';
|
|
4
4
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
5
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -19,9 +19,11 @@ import {
|
|
|
19
19
|
L2BlockStream,
|
|
20
20
|
type L2BlockStreamEvent,
|
|
21
21
|
type L2BlockStreamEventHandler,
|
|
22
|
-
|
|
22
|
+
getAttestationInfoFromPublishedCheckpoint,
|
|
23
23
|
} from '@aztec/stdlib/block';
|
|
24
|
+
import type { ChainConfig } from '@aztec/stdlib/config';
|
|
24
25
|
import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
26
|
+
import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
|
|
25
27
|
import type {
|
|
26
28
|
SingleValidatorStats,
|
|
27
29
|
ValidatorStats,
|
|
@@ -36,6 +38,23 @@ import EventEmitter from 'node:events';
|
|
|
36
38
|
|
|
37
39
|
import { SentinelStore } from './store.js';
|
|
38
40
|
|
|
41
|
+
export type SentinelRuntimeConfig = Pick<
|
|
42
|
+
SlasherConfig,
|
|
43
|
+
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashInactivityConsecutiveEpochThreshold'
|
|
44
|
+
> &
|
|
45
|
+
Pick<ChainConfig, 'l1ChainId' | 'l1Contracts'>;
|
|
46
|
+
|
|
47
|
+
/** Maps a validator status to its category: proposer or attestation. */
|
|
48
|
+
function statusToCategory(status: ValidatorStatusInSlot): ValidatorStatusType {
|
|
49
|
+
switch (status) {
|
|
50
|
+
case 'attestation-sent':
|
|
51
|
+
case 'attestation-missed':
|
|
52
|
+
return 'attestation';
|
|
53
|
+
default:
|
|
54
|
+
return 'proposer';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
export class Sentinel extends (EventEmitter as new () => WatcherEmitter) implements L2BlockStreamEventHandler, Watcher {
|
|
40
59
|
protected runningPromise: RunningPromise;
|
|
41
60
|
protected blockStream!: L2BlockStream;
|
|
@@ -44,18 +63,17 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
44
63
|
protected initialSlot: SlotNumber | undefined;
|
|
45
64
|
protected lastProcessedSlot: SlotNumber | undefined;
|
|
46
65
|
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
47
|
-
protected
|
|
48
|
-
|
|
66
|
+
protected slotNumberToCheckpoint: Map<
|
|
67
|
+
SlotNumber,
|
|
68
|
+
{ checkpointNumber: CheckpointNumber; archive: string; attestors: EthAddress[] }
|
|
69
|
+
> = new Map();
|
|
49
70
|
|
|
50
71
|
constructor(
|
|
51
72
|
protected epochCache: EpochCache,
|
|
52
73
|
protected archiver: L2BlockSource,
|
|
53
74
|
protected p2p: P2PClient,
|
|
54
75
|
protected store: SentinelStore,
|
|
55
|
-
protected config:
|
|
56
|
-
SlasherConfig,
|
|
57
|
-
'slashInactivityTargetPercentage' | 'slashInactivityPenalty' | 'slashInactivityConsecutiveEpochThreshold'
|
|
58
|
-
>,
|
|
76
|
+
protected config: SentinelRuntimeConfig,
|
|
59
77
|
protected logger = createLogger('node:sentinel'),
|
|
60
78
|
) {
|
|
61
79
|
super();
|
|
@@ -64,6 +82,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
64
82
|
this.runningPromise = new RunningPromise(this.work.bind(this), logger, interval);
|
|
65
83
|
}
|
|
66
84
|
|
|
85
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
86
|
+
return {
|
|
87
|
+
chainId: this.config.l1ChainId,
|
|
88
|
+
rollupAddress: this.config.l1Contracts.rollupAddress,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
67
92
|
public updateConfig(config: Partial<SlasherConfig>) {
|
|
68
93
|
this.config = { ...this.config, ...config };
|
|
69
94
|
}
|
|
@@ -75,8 +100,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
75
100
|
|
|
76
101
|
/** Loads initial slot and initializes blockstream. We will not process anything at or before the initial slot. */
|
|
77
102
|
protected async init() {
|
|
78
|
-
this.initialSlot = this.epochCache.
|
|
79
|
-
const startingBlock = await this.archiver.getBlockNumber();
|
|
103
|
+
this.initialSlot = this.epochCache.getSlotNow();
|
|
104
|
+
const startingBlock = BlockNumber(await this.archiver.getBlockNumber());
|
|
80
105
|
this.logger.info(`Starting validator sentinel with initial slot ${this.initialSlot} and block ${startingBlock}`);
|
|
81
106
|
this.blockStream = new L2BlockStream(this.archiver, this.l2TipsStore, this, this.logger, { startingBlock });
|
|
82
107
|
}
|
|
@@ -87,47 +112,54 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
87
112
|
|
|
88
113
|
public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
|
|
89
114
|
await this.l2TipsStore.handleBlockStreamEvent(event);
|
|
90
|
-
if (event.type === '
|
|
91
|
-
|
|
92
|
-
for (const block of event.blocks) {
|
|
93
|
-
this.slotNumberToBlock.set(block.block.header.getSlot(), {
|
|
94
|
-
blockNumber: block.block.number,
|
|
95
|
-
archive: block.block.archive.root.toString(),
|
|
96
|
-
attestors: getAttestationInfoFromPublishedL2Block(block)
|
|
97
|
-
.filter(a => a.status === 'recovered-from-signature')
|
|
98
|
-
.map(a => a.address!),
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Prune the archive map to only keep at most N entries
|
|
103
|
-
const historyLength = this.store.getHistoryLength();
|
|
104
|
-
if (this.slotNumberToBlock.size > historyLength) {
|
|
105
|
-
const toDelete = Array.from(this.slotNumberToBlock.keys())
|
|
106
|
-
.sort((a, b) => Number(a - b))
|
|
107
|
-
.slice(0, this.slotNumberToBlock.size - historyLength);
|
|
108
|
-
for (const key of toDelete) {
|
|
109
|
-
this.slotNumberToBlock.delete(key);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
115
|
+
if (event.type === 'chain-checkpointed') {
|
|
116
|
+
this.handleCheckpoint(event);
|
|
112
117
|
} else if (event.type === 'chain-proven') {
|
|
113
118
|
await this.handleChainProven(event);
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
|
|
122
|
+
protected handleCheckpoint(event: L2BlockStreamEvent) {
|
|
123
|
+
if (event.type !== 'chain-checkpointed') {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const checkpoint = event.checkpoint;
|
|
127
|
+
|
|
128
|
+
// Store mapping from slot to archive, checkpoint number, and attestors
|
|
129
|
+
this.slotNumberToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, {
|
|
130
|
+
checkpointNumber: checkpoint.checkpoint.number,
|
|
131
|
+
archive: checkpoint.checkpoint.archive.root.toString(),
|
|
132
|
+
attestors: getAttestationInfoFromPublishedCheckpoint(checkpoint, this.getSignatureContext())
|
|
133
|
+
.filter(a => a.status === 'recovered-from-signature')
|
|
134
|
+
.map(a => a.address!),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Prune the archive map to only keep at most N entries
|
|
138
|
+
const historyLength = this.store.getHistoryLength();
|
|
139
|
+
if (this.slotNumberToCheckpoint.size > historyLength) {
|
|
140
|
+
const toDelete = Array.from(this.slotNumberToCheckpoint.keys())
|
|
141
|
+
.sort((a, b) => Number(a - b))
|
|
142
|
+
.slice(0, this.slotNumberToCheckpoint.size - historyLength);
|
|
143
|
+
for (const key of toDelete) {
|
|
144
|
+
this.slotNumberToCheckpoint.delete(key);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
117
149
|
protected async handleChainProven(event: L2BlockStreamEvent) {
|
|
118
150
|
if (event.type !== 'chain-proven') {
|
|
119
151
|
return;
|
|
120
152
|
}
|
|
121
153
|
const blockNumber = event.block.number;
|
|
122
|
-
const
|
|
123
|
-
if (!
|
|
124
|
-
this.logger.error(`Failed to get block ${blockNumber}
|
|
154
|
+
const header = await this.archiver.getBlockHeader(blockNumber);
|
|
155
|
+
if (!header) {
|
|
156
|
+
this.logger.error(`Failed to get block header ${blockNumber}`);
|
|
125
157
|
return;
|
|
126
158
|
}
|
|
127
159
|
|
|
128
160
|
// TODO(palla/slash): We should only be computing proven performance if this is
|
|
129
161
|
// a full proof epoch and not a partial one, otherwise we'll end up with skewed stats.
|
|
130
|
-
const epoch = getEpochAtSlot(
|
|
162
|
+
const epoch = getEpochAtSlot(header.getSlot(), this.epochCache.getL1Constants());
|
|
131
163
|
this.logger.debug(`Computing proven performance for epoch ${epoch}`);
|
|
132
164
|
const performance = await this.computeProvenPerformance(epoch);
|
|
133
165
|
this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
|
|
@@ -138,7 +170,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
138
170
|
|
|
139
171
|
protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
|
|
140
172
|
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
|
|
141
|
-
const { committee } = await this.epochCache.getCommittee(fromSlot);
|
|
173
|
+
const { committee, isEscapeHatchOpen } = await this.epochCache.getCommittee(fromSlot);
|
|
174
|
+
if (isEscapeHatchOpen) {
|
|
175
|
+
this.logger.info(`Skipping proven performance for epoch ${epoch} - escape hatch is open`);
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
142
178
|
if (!committee) {
|
|
143
179
|
this.logger.trace(`No committee found for slot ${fromSlot}`);
|
|
144
180
|
return {};
|
|
@@ -195,7 +231,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
195
231
|
// Check that we have at least requiredConsecutiveEpochs and that all of them are above the inactivity threshold
|
|
196
232
|
return pastEpochs
|
|
197
233
|
.slice(0, requiredConsecutiveEpochs)
|
|
198
|
-
.every(p => p.missed / p.total >= this.config.slashInactivityTargetPercentage);
|
|
234
|
+
.every(p => (p.total === 0 ? false : p.missed / p.total >= this.config.slashInactivityTargetPercentage));
|
|
199
235
|
}
|
|
200
236
|
|
|
201
237
|
protected async handleProvenPerformance(epoch: EpochNumber, performance: ValidatorsEpochPerformance) {
|
|
@@ -204,7 +240,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
204
240
|
}
|
|
205
241
|
|
|
206
242
|
const inactiveValidators = getEntries(performance)
|
|
207
|
-
.filter(([_, { missed, total }]) => missed / total >= this.config.slashInactivityTargetPercentage)
|
|
243
|
+
.filter(([_, { missed, total }]) => total > 0 && missed / total >= this.config.slashInactivityTargetPercentage)
|
|
208
244
|
.map(([address]) => address);
|
|
209
245
|
|
|
210
246
|
this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
|
|
@@ -240,7 +276,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
240
276
|
* and we don't have that data if we were offline during the period.
|
|
241
277
|
*/
|
|
242
278
|
public async work() {
|
|
243
|
-
const
|
|
279
|
+
const currentSlot = this.epochCache.getSlotNow();
|
|
244
280
|
try {
|
|
245
281
|
// Manually sync the block stream to ensure we have the latest data.
|
|
246
282
|
// Note we never `start` the blockstream, so it loops at the same pace as we do.
|
|
@@ -285,14 +321,14 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
285
321
|
return false;
|
|
286
322
|
}
|
|
287
323
|
|
|
288
|
-
const
|
|
289
|
-
if (
|
|
290
|
-
this.logger.debug(`Waiting for archiver to sync with L2 slot ${targetSlot}`, {
|
|
324
|
+
const syncedSlot = await this.archiver.getSyncedL2SlotNumber();
|
|
325
|
+
if (syncedSlot === undefined || syncedSlot < targetSlot) {
|
|
326
|
+
this.logger.debug(`Waiting for archiver to sync with L2 slot ${targetSlot}`, { syncedSlot, targetSlot });
|
|
291
327
|
return false;
|
|
292
328
|
}
|
|
293
329
|
|
|
294
|
-
const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.
|
|
295
|
-
const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.
|
|
330
|
+
const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.proposed.hash);
|
|
331
|
+
const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.proposed.hash);
|
|
296
332
|
const isP2pSynced = archiverLastBlockHash === p2pLastBlockHash;
|
|
297
333
|
if (!isP2pSynced) {
|
|
298
334
|
this.logger.debug(`Waiting for P2P client to sync with archiver`, { archiverLastBlockHash, p2pLastBlockHash });
|
|
@@ -307,7 +343,12 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
307
343
|
* and updates overall stats.
|
|
308
344
|
*/
|
|
309
345
|
protected async processSlot(slot: SlotNumber) {
|
|
310
|
-
const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
|
|
346
|
+
const { epoch, seed, committee, isEscapeHatchOpen } = await this.epochCache.getCommittee(slot);
|
|
347
|
+
if (isEscapeHatchOpen) {
|
|
348
|
+
this.logger.info(`Skipping slot ${slot} at epoch ${epoch} - escape hatch is open`);
|
|
349
|
+
this.lastProcessedSlot = slot;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
311
352
|
if (!committee || committee.length === 0) {
|
|
312
353
|
this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
|
|
313
354
|
this.lastProcessedSlot = slot;
|
|
@@ -327,16 +368,16 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
327
368
|
|
|
328
369
|
// Check if there is an L2 block in L1 for this L2 slot
|
|
329
370
|
|
|
330
|
-
// Here we get all attestations for the
|
|
331
|
-
// or all attestations for all proposals in the slot if no
|
|
371
|
+
// Here we get all checkpoint attestations for the checkpoint at the given slot,
|
|
372
|
+
// or all checkpoint attestations for all proposals in the slot if no checkpoint was mined.
|
|
332
373
|
// We gather from both p2p (contains the ones seen on the p2p layer) and archiver
|
|
333
|
-
// (contains the ones synced from mined
|
|
334
|
-
const
|
|
335
|
-
const p2pAttested = await this.p2p.
|
|
374
|
+
// (contains the ones synced from mined checkpoints, which we may have missed from p2p).
|
|
375
|
+
const checkpoint = this.slotNumberToCheckpoint.get(slot);
|
|
376
|
+
const p2pAttested = await this.p2p.getCheckpointAttestationsForSlot(slot, checkpoint?.archive);
|
|
336
377
|
// Filter out attestations with invalid signatures
|
|
337
378
|
const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
|
|
338
379
|
const attestors = new Set(
|
|
339
|
-
[...p2pAttestors.map(a => a.toString()), ...(
|
|
380
|
+
[...p2pAttestors.map(a => a.toString()), ...(checkpoint?.attestors.map(a => a.toString()) ?? [])].filter(
|
|
340
381
|
addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
|
|
341
382
|
),
|
|
342
383
|
);
|
|
@@ -347,20 +388,29 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
347
388
|
// But we'll leave that corner case out to reduce pressure on the node.
|
|
348
389
|
// TODO(palla/slash): This breaks if a given node has more than one validator in the current committee,
|
|
349
390
|
// since they will attest to their own proposal it even if it's not re-executable.
|
|
350
|
-
|
|
351
|
-
|
|
391
|
+
let status: 'checkpoint-mined' | 'checkpoint-proposed' | 'checkpoint-missed' | 'blocks-missed';
|
|
392
|
+
if (checkpoint) {
|
|
393
|
+
status = 'checkpoint-mined';
|
|
394
|
+
} else if (attestors.size > 0) {
|
|
395
|
+
status = 'checkpoint-proposed';
|
|
396
|
+
} else {
|
|
397
|
+
// No checkpoint on L1 and no checkpoint attestations seen. Check if block proposals were sent for this slot.
|
|
398
|
+
const hasBlockProposals = await this.p2p.hasBlockProposalsForSlot(slot);
|
|
399
|
+
status = hasBlockProposals ? 'checkpoint-missed' : 'blocks-missed';
|
|
400
|
+
}
|
|
401
|
+
this.logger.debug(`Checkpoint status for slot ${slot}: ${status}`, { ...checkpoint, slot });
|
|
352
402
|
|
|
353
|
-
// Get attestors that failed their
|
|
403
|
+
// Get attestors that failed their checkpoint attestation duties, but only if there was a checkpoint proposed or mined
|
|
354
404
|
const missedAttestors = new Set(
|
|
355
|
-
|
|
405
|
+
status === 'blocks-missed' || status === 'checkpoint-missed'
|
|
356
406
|
? []
|
|
357
407
|
: committee.filter(v => !attestors.has(v.toString()) && !proposer.equals(v)).map(v => v.toString()),
|
|
358
408
|
);
|
|
359
409
|
|
|
360
410
|
this.logger.debug(`Retrieved ${attestors.size} attestors out of ${committee.length} for slot ${slot}`, {
|
|
361
|
-
|
|
411
|
+
status,
|
|
362
412
|
proposer: proposer.toString(),
|
|
363
|
-
...
|
|
413
|
+
...checkpoint,
|
|
364
414
|
slot,
|
|
365
415
|
attestors: [...attestors],
|
|
366
416
|
missedAttestors: [...missedAttestors],
|
|
@@ -370,7 +420,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
370
420
|
// Compute the status for each validator in the committee
|
|
371
421
|
const statusFor = (who: `0x${string}`): ValidatorStatusInSlot | undefined => {
|
|
372
422
|
if (who === proposer.toString()) {
|
|
373
|
-
return
|
|
423
|
+
return status;
|
|
374
424
|
} else if (attestors.has(who)) {
|
|
375
425
|
return 'attestation-sent';
|
|
376
426
|
} else if (missedAttestors.has(who)) {
|
|
@@ -398,7 +448,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
398
448
|
? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
|
|
399
449
|
: await this.store.getHistories();
|
|
400
450
|
|
|
401
|
-
const slotNow = this.epochCache.
|
|
451
|
+
const slotNow = this.epochCache.getSlotNow();
|
|
402
452
|
fromSlot ??= SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
|
|
403
453
|
toSlot ??= this.lastProcessedSlot ?? slotNow;
|
|
404
454
|
|
|
@@ -426,7 +476,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
426
476
|
return undefined;
|
|
427
477
|
}
|
|
428
478
|
|
|
429
|
-
const slotNow = this.epochCache.
|
|
479
|
+
const slotNow = this.epochCache.getSlotNow();
|
|
430
480
|
const effectiveFromSlot =
|
|
431
481
|
fromSlot ?? SlotNumber(Math.max((this.lastProcessedSlot ?? slotNow) - this.store.getHistoryLength(), 0));
|
|
432
482
|
const effectiveToSlot = toSlot ?? this.lastProcessedSlot ?? slotNow;
|
|
@@ -463,14 +513,16 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
463
513
|
): ValidatorStats {
|
|
464
514
|
let history = fromSlot ? allHistory.filter(h => BigInt(h.slot) >= fromSlot) : allHistory;
|
|
465
515
|
history = toSlot ? history.filter(h => BigInt(h.slot) <= toSlot) : history;
|
|
466
|
-
const lastProposal = history
|
|
516
|
+
const lastProposal = history
|
|
517
|
+
.filter(h => h.status === 'checkpoint-proposed' || h.status === 'checkpoint-mined')
|
|
518
|
+
.at(-1);
|
|
467
519
|
const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
|
|
468
520
|
return {
|
|
469
521
|
address: EthAddress.fromString(address),
|
|
470
522
|
lastProposal: this.computeFromSlot(lastProposal?.slot),
|
|
471
523
|
lastAttestation: this.computeFromSlot(lastAttestation?.slot),
|
|
472
524
|
totalSlots: history.length,
|
|
473
|
-
missedProposals: this.computeMissed(history, '
|
|
525
|
+
missedProposals: this.computeMissed(history, 'proposer', ['checkpoint-missed', 'blocks-missed']),
|
|
474
526
|
missedAttestations: this.computeMissed(history, 'attestation', ['attestation-missed']),
|
|
475
527
|
history,
|
|
476
528
|
};
|
|
@@ -478,10 +530,12 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
478
530
|
|
|
479
531
|
protected computeMissed(
|
|
480
532
|
history: ValidatorStatusHistory,
|
|
481
|
-
|
|
533
|
+
computeOverCategory: ValidatorStatusType | undefined,
|
|
482
534
|
filter: ValidatorStatusInSlot[],
|
|
483
535
|
) {
|
|
484
|
-
const relevantHistory = history.filter(
|
|
536
|
+
const relevantHistory = history.filter(
|
|
537
|
+
h => !computeOverCategory || statusToCategory(h.status) === computeOverCategory,
|
|
538
|
+
);
|
|
485
539
|
const filteredHistory = relevantHistory.filter(h => filter.includes(h.status));
|
|
486
540
|
return {
|
|
487
541
|
currentStreak: countWhile([...relevantHistory].reverse(), h => filter.includes(h.status)),
|
package/src/sentinel/store.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from '@aztec/stdlib/validators';
|
|
10
10
|
|
|
11
11
|
export class SentinelStore {
|
|
12
|
-
public static readonly SCHEMA_VERSION =
|
|
12
|
+
public static readonly SCHEMA_VERSION = 3;
|
|
13
13
|
|
|
14
14
|
// a map from validator address to their ValidatorStatusHistory
|
|
15
15
|
private readonly historyMap: AztecAsyncMap<`0x${string}`, Buffer>;
|
|
@@ -86,11 +86,7 @@ export class SentinelStore {
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
private async pushValidatorStatusForSlot(
|
|
90
|
-
who: EthAddress,
|
|
91
|
-
slot: SlotNumber,
|
|
92
|
-
status: 'block-mined' | 'block-proposed' | 'block-missed' | 'attestation-sent' | 'attestation-missed',
|
|
93
|
-
) {
|
|
89
|
+
private async pushValidatorStatusForSlot(who: EthAddress, slot: SlotNumber, status: ValidatorStatusInSlot) {
|
|
94
90
|
await this.store.transactionAsync(async () => {
|
|
95
91
|
const currentHistory = (await this.getHistory(who)) ?? [];
|
|
96
92
|
const newHistory = [...currentHistory, { slot, status }].slice(-this.config.historyLength);
|
|
@@ -149,16 +145,18 @@ export class SentinelStore {
|
|
|
149
145
|
|
|
150
146
|
private statusToNumber(status: ValidatorStatusInSlot): number {
|
|
151
147
|
switch (status) {
|
|
152
|
-
case '
|
|
148
|
+
case 'checkpoint-mined':
|
|
153
149
|
return 1;
|
|
154
|
-
case '
|
|
150
|
+
case 'checkpoint-proposed':
|
|
155
151
|
return 2;
|
|
156
|
-
case '
|
|
152
|
+
case 'checkpoint-missed':
|
|
157
153
|
return 3;
|
|
158
154
|
case 'attestation-sent':
|
|
159
155
|
return 4;
|
|
160
156
|
case 'attestation-missed':
|
|
161
157
|
return 5;
|
|
158
|
+
case 'blocks-missed':
|
|
159
|
+
return 6;
|
|
162
160
|
default: {
|
|
163
161
|
const _exhaustive: never = status;
|
|
164
162
|
throw new Error(`Unknown status: ${status}`);
|
|
@@ -169,15 +167,17 @@ export class SentinelStore {
|
|
|
169
167
|
private statusFromNumber(status: number): ValidatorStatusInSlot {
|
|
170
168
|
switch (status) {
|
|
171
169
|
case 1:
|
|
172
|
-
return '
|
|
170
|
+
return 'checkpoint-mined';
|
|
173
171
|
case 2:
|
|
174
|
-
return '
|
|
172
|
+
return 'checkpoint-proposed';
|
|
175
173
|
case 3:
|
|
176
|
-
return '
|
|
174
|
+
return 'checkpoint-missed';
|
|
177
175
|
case 4:
|
|
178
176
|
return 'attestation-sent';
|
|
179
177
|
case 5:
|
|
180
178
|
return 'attestation-missed';
|
|
179
|
+
case 6:
|
|
180
|
+
return 'blocks-missed';
|
|
181
181
|
default:
|
|
182
182
|
throw new Error(`Unknown status: ${status}`);
|
|
183
183
|
}
|