@aztec/aztec-node 0.0.1-commit.03f7ef2 → 0.0.1-commit.08c5969dc
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/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 +41 -99
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +579 -188
- package/dest/sentinel/factory.d.ts +1 -1
- package/dest/sentinel/factory.d.ts.map +1 -1
- package/dest/sentinel/factory.js +1 -1
- package/dest/sentinel/sentinel.d.ts +6 -5
- package/dest/sentinel/sentinel.d.ts.map +1 -1
- package/dest/sentinel/sentinel.js +67 -43
- 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 +26 -26
- package/src/aztec-node/node_metrics.ts +6 -17
- package/src/aztec-node/server.ts +242 -248
- package/src/sentinel/factory.ts +1 -6
- package/src/sentinel/sentinel.ts +80 -47
- package/src/sentinel/store.ts +12 -12
package/src/sentinel/factory.ts
CHANGED
|
@@ -20,12 +20,7 @@ export async function createSentinel(
|
|
|
20
20
|
if (!config.sentinelEnabled) {
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
-
const kvStore = await createStore(
|
|
24
|
-
'sentinel',
|
|
25
|
-
SentinelStore.SCHEMA_VERSION,
|
|
26
|
-
config,
|
|
27
|
-
createLogger('node:sentinel:lmdb'),
|
|
28
|
-
);
|
|
23
|
+
const kvStore = await createStore('sentinel', SentinelStore.SCHEMA_VERSION, config, logger.getBindings());
|
|
29
24
|
const storeHistoryLength = config.sentinelHistoryLengthInEpochs * epochCache.getL1Constants().epochDuration;
|
|
30
25
|
const storeHistoricProvenPerformanceLength = config.sentinelHistoricProvenPerformanceLengthInEpochs;
|
|
31
26
|
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 { BlockNumber, 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,7 +19,7 @@ import {
|
|
|
19
19
|
L2BlockStream,
|
|
20
20
|
type L2BlockStreamEvent,
|
|
21
21
|
type L2BlockStreamEventHandler,
|
|
22
|
-
|
|
22
|
+
getAttestationInfoFromPublishedCheckpoint,
|
|
23
23
|
} from '@aztec/stdlib/block';
|
|
24
24
|
import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
25
25
|
import type {
|
|
@@ -36,6 +36,17 @@ import EventEmitter from 'node:events';
|
|
|
36
36
|
|
|
37
37
|
import { SentinelStore } from './store.js';
|
|
38
38
|
|
|
39
|
+
/** Maps a validator status to its category: proposer or attestation. */
|
|
40
|
+
function statusToCategory(status: ValidatorStatusInSlot): ValidatorStatusType {
|
|
41
|
+
switch (status) {
|
|
42
|
+
case 'attestation-sent':
|
|
43
|
+
case 'attestation-missed':
|
|
44
|
+
return 'attestation';
|
|
45
|
+
default:
|
|
46
|
+
return 'proposer';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
export class Sentinel extends (EventEmitter as new () => WatcherEmitter) implements L2BlockStreamEventHandler, Watcher {
|
|
40
51
|
protected runningPromise: RunningPromise;
|
|
41
52
|
protected blockStream!: L2BlockStream;
|
|
@@ -44,8 +55,10 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
44
55
|
protected initialSlot: SlotNumber | undefined;
|
|
45
56
|
protected lastProcessedSlot: SlotNumber | undefined;
|
|
46
57
|
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
47
|
-
protected
|
|
48
|
-
|
|
58
|
+
protected slotNumberToCheckpoint: Map<
|
|
59
|
+
SlotNumber,
|
|
60
|
+
{ checkpointNumber: CheckpointNumber; archive: string; attestors: EthAddress[] }
|
|
61
|
+
> = new Map();
|
|
49
62
|
|
|
50
63
|
constructor(
|
|
51
64
|
protected epochCache: EpochCache,
|
|
@@ -87,39 +100,46 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
87
100
|
|
|
88
101
|
public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
|
|
89
102
|
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: 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
|
-
}
|
|
103
|
+
if (event.type === 'chain-checkpointed') {
|
|
104
|
+
this.handleCheckpoint(event);
|
|
112
105
|
} else if (event.type === 'chain-proven') {
|
|
113
106
|
await this.handleChainProven(event);
|
|
114
107
|
}
|
|
115
108
|
}
|
|
116
109
|
|
|
110
|
+
protected handleCheckpoint(event: L2BlockStreamEvent) {
|
|
111
|
+
if (event.type !== 'chain-checkpointed') {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const checkpoint = event.checkpoint;
|
|
115
|
+
|
|
116
|
+
// Store mapping from slot to archive, checkpoint number, and attestors
|
|
117
|
+
this.slotNumberToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, {
|
|
118
|
+
checkpointNumber: checkpoint.checkpoint.number,
|
|
119
|
+
archive: checkpoint.checkpoint.archive.root.toString(),
|
|
120
|
+
attestors: getAttestationInfoFromPublishedCheckpoint(checkpoint)
|
|
121
|
+
.filter(a => a.status === 'recovered-from-signature')
|
|
122
|
+
.map(a => a.address!),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Prune the archive map to only keep at most N entries
|
|
126
|
+
const historyLength = this.store.getHistoryLength();
|
|
127
|
+
if (this.slotNumberToCheckpoint.size > historyLength) {
|
|
128
|
+
const toDelete = Array.from(this.slotNumberToCheckpoint.keys())
|
|
129
|
+
.sort((a, b) => Number(a - b))
|
|
130
|
+
.slice(0, this.slotNumberToCheckpoint.size - historyLength);
|
|
131
|
+
for (const key of toDelete) {
|
|
132
|
+
this.slotNumberToCheckpoint.delete(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
protected async handleChainProven(event: L2BlockStreamEvent) {
|
|
118
138
|
if (event.type !== 'chain-proven') {
|
|
119
139
|
return;
|
|
120
140
|
}
|
|
121
|
-
const blockNumber =
|
|
122
|
-
const block = await this.archiver.
|
|
141
|
+
const blockNumber = event.block.number;
|
|
142
|
+
const block = await this.archiver.getL2Block(blockNumber);
|
|
123
143
|
if (!block) {
|
|
124
144
|
this.logger.error(`Failed to get block ${blockNumber}`, { block });
|
|
125
145
|
return;
|
|
@@ -291,8 +311,8 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
291
311
|
return false;
|
|
292
312
|
}
|
|
293
313
|
|
|
294
|
-
const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.
|
|
295
|
-
const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.
|
|
314
|
+
const archiverLastBlockHash = await this.l2TipsStore.getL2Tips().then(tip => tip.proposed.hash);
|
|
315
|
+
const p2pLastBlockHash = await this.p2p.getL2Tips().then(tips => tips.proposed.hash);
|
|
296
316
|
const isP2pSynced = archiverLastBlockHash === p2pLastBlockHash;
|
|
297
317
|
if (!isP2pSynced) {
|
|
298
318
|
this.logger.debug(`Waiting for P2P client to sync with archiver`, { archiverLastBlockHash, p2pLastBlockHash });
|
|
@@ -327,16 +347,16 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
327
347
|
|
|
328
348
|
// Check if there is an L2 block in L1 for this L2 slot
|
|
329
349
|
|
|
330
|
-
// Here we get all attestations for the
|
|
331
|
-
// or all attestations for all proposals in the slot if no
|
|
350
|
+
// Here we get all checkpoint attestations for the checkpoint at the given slot,
|
|
351
|
+
// or all checkpoint attestations for all proposals in the slot if no checkpoint was mined.
|
|
332
352
|
// 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.
|
|
353
|
+
// (contains the ones synced from mined checkpoints, which we may have missed from p2p).
|
|
354
|
+
const checkpoint = this.slotNumberToCheckpoint.get(slot);
|
|
355
|
+
const p2pAttested = await this.p2p.getCheckpointAttestationsForSlot(slot, checkpoint?.archive);
|
|
336
356
|
// Filter out attestations with invalid signatures
|
|
337
357
|
const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
|
|
338
358
|
const attestors = new Set(
|
|
339
|
-
[...p2pAttestors.map(a => a.toString()), ...(
|
|
359
|
+
[...p2pAttestors.map(a => a.toString()), ...(checkpoint?.attestors.map(a => a.toString()) ?? [])].filter(
|
|
340
360
|
addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
|
|
341
361
|
),
|
|
342
362
|
);
|
|
@@ -347,20 +367,29 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
347
367
|
// But we'll leave that corner case out to reduce pressure on the node.
|
|
348
368
|
// TODO(palla/slash): This breaks if a given node has more than one validator in the current committee,
|
|
349
369
|
// since they will attest to their own proposal it even if it's not re-executable.
|
|
350
|
-
|
|
351
|
-
|
|
370
|
+
let status: 'checkpoint-mined' | 'checkpoint-proposed' | 'checkpoint-missed' | 'blocks-missed';
|
|
371
|
+
if (checkpoint) {
|
|
372
|
+
status = 'checkpoint-mined';
|
|
373
|
+
} else if (attestors.size > 0) {
|
|
374
|
+
status = 'checkpoint-proposed';
|
|
375
|
+
} else {
|
|
376
|
+
// No checkpoint on L1 and no checkpoint attestations seen. Check if block proposals were sent for this slot.
|
|
377
|
+
const hasBlockProposals = await this.p2p.hasBlockProposalsForSlot(slot);
|
|
378
|
+
status = hasBlockProposals ? 'checkpoint-missed' : 'blocks-missed';
|
|
379
|
+
}
|
|
380
|
+
this.logger.debug(`Checkpoint status for slot ${slot}: ${status}`, { ...checkpoint, slot });
|
|
352
381
|
|
|
353
|
-
// Get attestors that failed their
|
|
382
|
+
// Get attestors that failed their checkpoint attestation duties, but only if there was a checkpoint proposed or mined
|
|
354
383
|
const missedAttestors = new Set(
|
|
355
|
-
|
|
384
|
+
status === 'blocks-missed' || status === 'checkpoint-missed'
|
|
356
385
|
? []
|
|
357
386
|
: committee.filter(v => !attestors.has(v.toString()) && !proposer.equals(v)).map(v => v.toString()),
|
|
358
387
|
);
|
|
359
388
|
|
|
360
389
|
this.logger.debug(`Retrieved ${attestors.size} attestors out of ${committee.length} for slot ${slot}`, {
|
|
361
|
-
|
|
390
|
+
status,
|
|
362
391
|
proposer: proposer.toString(),
|
|
363
|
-
...
|
|
392
|
+
...checkpoint,
|
|
364
393
|
slot,
|
|
365
394
|
attestors: [...attestors],
|
|
366
395
|
missedAttestors: [...missedAttestors],
|
|
@@ -370,7 +399,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
370
399
|
// Compute the status for each validator in the committee
|
|
371
400
|
const statusFor = (who: `0x${string}`): ValidatorStatusInSlot | undefined => {
|
|
372
401
|
if (who === proposer.toString()) {
|
|
373
|
-
return
|
|
402
|
+
return status;
|
|
374
403
|
} else if (attestors.has(who)) {
|
|
375
404
|
return 'attestation-sent';
|
|
376
405
|
} else if (missedAttestors.has(who)) {
|
|
@@ -463,14 +492,16 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
463
492
|
): ValidatorStats {
|
|
464
493
|
let history = fromSlot ? allHistory.filter(h => BigInt(h.slot) >= fromSlot) : allHistory;
|
|
465
494
|
history = toSlot ? history.filter(h => BigInt(h.slot) <= toSlot) : history;
|
|
466
|
-
const lastProposal = history
|
|
495
|
+
const lastProposal = history
|
|
496
|
+
.filter(h => h.status === 'checkpoint-proposed' || h.status === 'checkpoint-mined')
|
|
497
|
+
.at(-1);
|
|
467
498
|
const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
|
|
468
499
|
return {
|
|
469
500
|
address: EthAddress.fromString(address),
|
|
470
501
|
lastProposal: this.computeFromSlot(lastProposal?.slot),
|
|
471
502
|
lastAttestation: this.computeFromSlot(lastAttestation?.slot),
|
|
472
503
|
totalSlots: history.length,
|
|
473
|
-
missedProposals: this.computeMissed(history, '
|
|
504
|
+
missedProposals: this.computeMissed(history, 'proposer', ['checkpoint-missed', 'blocks-missed']),
|
|
474
505
|
missedAttestations: this.computeMissed(history, 'attestation', ['attestation-missed']),
|
|
475
506
|
history,
|
|
476
507
|
};
|
|
@@ -478,10 +509,12 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
478
509
|
|
|
479
510
|
protected computeMissed(
|
|
480
511
|
history: ValidatorStatusHistory,
|
|
481
|
-
|
|
512
|
+
computeOverCategory: ValidatorStatusType | undefined,
|
|
482
513
|
filter: ValidatorStatusInSlot[],
|
|
483
514
|
) {
|
|
484
|
-
const relevantHistory = history.filter(
|
|
515
|
+
const relevantHistory = history.filter(
|
|
516
|
+
h => !computeOverCategory || statusToCategory(h.status) === computeOverCategory,
|
|
517
|
+
);
|
|
485
518
|
const filteredHistory = relevantHistory.filter(h => filter.includes(h.status));
|
|
486
519
|
return {
|
|
487
520
|
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
|
}
|