@aztec/aztec-node 3.0.0-canary.a9708bd → 3.0.0-manual.20251030
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 +4 -2
- package/dest/aztec-node/config.d.ts.map +1 -1
- package/dest/aztec-node/config.js +15 -19
- package/dest/aztec-node/server.d.ts +29 -3
- package/dest/aztec-node/server.d.ts.map +1 -1
- package/dest/aztec-node/server.js +117 -39
- package/dest/sentinel/config.d.ts +1 -0
- package/dest/sentinel/config.d.ts.map +1 -1
- package/dest/sentinel/config.js +16 -0
- package/dest/sentinel/factory.d.ts.map +1 -1
- package/dest/sentinel/factory.js +3 -1
- package/dest/sentinel/sentinel.d.ts +4 -3
- package/dest/sentinel/sentinel.d.ts.map +1 -1
- package/dest/sentinel/sentinel.js +64 -65
- package/dest/sentinel/store.d.ts +2 -0
- package/dest/sentinel/store.d.ts.map +1 -1
- package/dest/sentinel/store.js +5 -2
- package/package.json +26 -26
- package/src/aztec-node/config.ts +26 -35
- package/src/aztec-node/server.ts +141 -53
- package/src/sentinel/config.ts +18 -0
- package/src/sentinel/factory.ts +5 -1
- package/src/sentinel/sentinel.ts +80 -75
- package/src/sentinel/store.ts +7 -3
package/src/aztec-node/server.ts
CHANGED
|
@@ -11,14 +11,12 @@ import {
|
|
|
11
11
|
} from '@aztec/constants';
|
|
12
12
|
import { EpochCache, type EpochCacheInterface } from '@aztec/epoch-cache';
|
|
13
13
|
import {
|
|
14
|
-
type EthSigner,
|
|
15
14
|
type L1ContractAddresses,
|
|
16
15
|
RegistryContract,
|
|
17
16
|
RollupContract,
|
|
18
17
|
createEthereumChain,
|
|
19
18
|
getPublicClient,
|
|
20
19
|
} from '@aztec/ethereum';
|
|
21
|
-
import { createL1TxUtilsWithBlobsFromEthSigner } from '@aztec/ethereum/l1-tx-utils-with-blobs';
|
|
22
20
|
import { compactArray, pick } from '@aztec/foundation/collection';
|
|
23
21
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
24
22
|
import { Fr } from '@aztec/foundation/fields';
|
|
@@ -30,6 +28,7 @@ import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
|
30
28
|
import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees';
|
|
31
29
|
import { KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
|
|
32
30
|
import { trySnapshotSync, uploadSnapshot } from '@aztec/node-lib/actions';
|
|
31
|
+
import { createL1TxUtilsWithBlobsFromEthSigner } from '@aztec/node-lib/factories';
|
|
33
32
|
import { type P2P, type P2PClientDeps, createP2PClient, getDefaultAllowedSetupFunctions } from '@aztec/p2p';
|
|
34
33
|
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
|
|
35
34
|
import {
|
|
@@ -74,6 +73,7 @@ import {
|
|
|
74
73
|
type GetPublicLogsResponse,
|
|
75
74
|
} from '@aztec/stdlib/interfaces/client';
|
|
76
75
|
import {
|
|
76
|
+
type AllowedElement,
|
|
77
77
|
type ClientProtocolCircuitVerifier,
|
|
78
78
|
type L2LogsSource,
|
|
79
79
|
type Service,
|
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
tryStop,
|
|
83
83
|
} from '@aztec/stdlib/interfaces/server';
|
|
84
84
|
import type { LogFilter, PrivateLog, TxScopedL2Log } from '@aztec/stdlib/logs';
|
|
85
|
-
import type
|
|
85
|
+
import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
86
86
|
import { P2PClientType } from '@aztec/stdlib/p2p';
|
|
87
87
|
import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing';
|
|
88
88
|
import type { NullifierLeafPreimage, PublicDataTreeLeaf, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees';
|
|
@@ -108,7 +108,12 @@ import {
|
|
|
108
108
|
getTelemetryClient,
|
|
109
109
|
trackSpan,
|
|
110
110
|
} from '@aztec/telemetry-client';
|
|
111
|
-
import {
|
|
111
|
+
import {
|
|
112
|
+
NodeKeystoreAdapter,
|
|
113
|
+
ValidatorClient,
|
|
114
|
+
createBlockProposalHandler,
|
|
115
|
+
createValidatorClient,
|
|
116
|
+
} from '@aztec/validator-client';
|
|
112
117
|
import { createWorldStateSynchronizer } from '@aztec/world-state';
|
|
113
118
|
|
|
114
119
|
import { createPublicClient, fallback, http } from 'viem';
|
|
@@ -212,6 +217,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
await keyStoreManager?.validateSigners();
|
|
221
|
+
|
|
215
222
|
// If we are a validator, verify our configuration before doing too much more.
|
|
216
223
|
if (!config.disableValidator) {
|
|
217
224
|
if (keyStoreManager === undefined) {
|
|
@@ -222,7 +229,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
222
229
|
'KEY STORE CREATED FROM ENVIRONMENT, IT IS RECOMMENDED TO USE A FILE-BASED KEY STORE IN PRODUCTION ENVIRONMENTS',
|
|
223
230
|
);
|
|
224
231
|
}
|
|
225
|
-
ValidatorClient.validateKeyStoreConfiguration(keyStoreManager);
|
|
232
|
+
ValidatorClient.validateKeyStoreConfiguration(keyStoreManager, log);
|
|
226
233
|
}
|
|
227
234
|
|
|
228
235
|
// validate that the actual chain id matches that specified in configuration
|
|
@@ -270,7 +277,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
270
277
|
const archiver = await createArchiver(
|
|
271
278
|
config,
|
|
272
279
|
{ blobSinkClient, epochCache, telemetry, dateProvider },
|
|
273
|
-
{ blockUntilSync:
|
|
280
|
+
{ blockUntilSync: !config.skipArchiverInitialSync },
|
|
274
281
|
);
|
|
275
282
|
|
|
276
283
|
// now create the merkle trees and the world state synchronizer
|
|
@@ -300,12 +307,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
300
307
|
deps.p2pClientDeps,
|
|
301
308
|
);
|
|
302
309
|
|
|
303
|
-
//
|
|
304
|
-
await worldStateSynchronizer.start();
|
|
305
|
-
|
|
306
|
-
// Start p2p. Note that it depends on world state to be running.
|
|
307
|
-
await p2pClient.start();
|
|
308
|
-
|
|
310
|
+
// We should really not be modifying the config object
|
|
309
311
|
config.txPublicSetupAllowList = config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
|
|
310
312
|
|
|
311
313
|
const blockBuilder = new BlockBuilder(
|
|
@@ -316,17 +318,57 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
316
318
|
telemetry,
|
|
317
319
|
);
|
|
318
320
|
|
|
321
|
+
// We'll accumulate sentinel watchers here
|
|
319
322
|
const watchers: Watcher[] = [];
|
|
320
323
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
324
|
+
// Create validator client if required
|
|
325
|
+
const validatorClient = createValidatorClient(config, {
|
|
326
|
+
p2pClient,
|
|
327
|
+
telemetry,
|
|
328
|
+
dateProvider,
|
|
329
|
+
epochCache,
|
|
330
|
+
blockBuilder,
|
|
331
|
+
blockSource: archiver,
|
|
332
|
+
l1ToL2MessageSource: archiver,
|
|
333
|
+
keyStoreManager,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// If we have a validator client, register it as a source of offenses for the slasher,
|
|
337
|
+
// and have it register callbacks on the p2p client *before* we start it, otherwise messages
|
|
338
|
+
// like attestations or auths will fail.
|
|
339
|
+
if (validatorClient) {
|
|
340
|
+
watchers.push(validatorClient);
|
|
341
|
+
if (!options.dontStartSequencer) {
|
|
342
|
+
await validatorClient.registerHandlers();
|
|
327
343
|
}
|
|
328
344
|
}
|
|
329
345
|
|
|
346
|
+
// If there's no validator client but alwaysReexecuteBlockProposals is enabled,
|
|
347
|
+
// create a BlockProposalHandler to reexecute block proposals for monitoring
|
|
348
|
+
if (!validatorClient && config.alwaysReexecuteBlockProposals) {
|
|
349
|
+
log.info('Setting up block proposal reexecution for monitoring');
|
|
350
|
+
createBlockProposalHandler(config, {
|
|
351
|
+
blockBuilder,
|
|
352
|
+
epochCache,
|
|
353
|
+
blockSource: archiver,
|
|
354
|
+
l1ToL2MessageSource: archiver,
|
|
355
|
+
p2pClient,
|
|
356
|
+
dateProvider,
|
|
357
|
+
telemetry,
|
|
358
|
+
}).registerForReexecution(p2pClient);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Start world state and wait for it to sync to the archiver.
|
|
362
|
+
await worldStateSynchronizer.start();
|
|
363
|
+
|
|
364
|
+
// Start p2p. Note that it depends on world state to be running.
|
|
365
|
+
await p2pClient.start();
|
|
366
|
+
|
|
367
|
+
const validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
|
|
368
|
+
if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
|
|
369
|
+
watchers.push(validatorsSentinel);
|
|
370
|
+
}
|
|
371
|
+
|
|
330
372
|
let epochPruneWatcher: EpochPruneWatcher | undefined;
|
|
331
373
|
if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
|
|
332
374
|
epochPruneWatcher = new EpochPruneWatcher(
|
|
@@ -337,7 +379,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
337
379
|
blockBuilder,
|
|
338
380
|
config,
|
|
339
381
|
);
|
|
340
|
-
await epochPruneWatcher.start();
|
|
341
382
|
watchers.push(epochPruneWatcher);
|
|
342
383
|
}
|
|
343
384
|
|
|
@@ -345,31 +386,24 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
345
386
|
let attestationsBlockWatcher: AttestationsBlockWatcher | undefined;
|
|
346
387
|
if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
|
|
347
388
|
attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
|
|
348
|
-
await attestationsBlockWatcher.start();
|
|
349
389
|
watchers.push(attestationsBlockWatcher);
|
|
350
390
|
}
|
|
351
391
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if (validatorClient) {
|
|
364
|
-
watchers.push(validatorClient);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
log.verbose(`All Aztec Node subsystems synced`);
|
|
392
|
+
// Start p2p-related services once the archiver has completed sync
|
|
393
|
+
void archiver
|
|
394
|
+
.waitForInitialSync()
|
|
395
|
+
.then(async () => {
|
|
396
|
+
await p2pClient.start();
|
|
397
|
+
await validatorsSentinel?.start();
|
|
398
|
+
await epochPruneWatcher?.start();
|
|
399
|
+
await attestationsBlockWatcher?.start();
|
|
400
|
+
log.info(`All p2p services started`);
|
|
401
|
+
})
|
|
402
|
+
.catch(err => log.error('Failed to start p2p services after archiver sync', err));
|
|
368
403
|
|
|
369
404
|
// Validator enabled, create/start relevant service
|
|
370
405
|
let sequencer: SequencerClient | undefined;
|
|
371
406
|
let slasherClient: SlasherClientInterface | undefined;
|
|
372
|
-
|
|
373
407
|
if (!config.disableValidator) {
|
|
374
408
|
// We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
|
|
375
409
|
// as they are executed when the node is selected as proposer.
|
|
@@ -389,13 +423,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
389
423
|
);
|
|
390
424
|
await slasherClient.start();
|
|
391
425
|
|
|
392
|
-
const l1TxUtils =
|
|
393
|
-
|
|
394
|
-
|
|
426
|
+
const l1TxUtils = await createL1TxUtilsWithBlobsFromEthSigner(
|
|
427
|
+
publicClient,
|
|
428
|
+
keyStoreManager!.createAllValidatorPublisherSigners(),
|
|
429
|
+
{ ...config, scope: 'sequencer' },
|
|
430
|
+
{ telemetry, logger: log.createChild('l1-tx-utils'), dateProvider },
|
|
431
|
+
);
|
|
395
432
|
|
|
433
|
+
// Create and start the sequencer client
|
|
396
434
|
sequencer = await SequencerClient.new(config, {
|
|
397
|
-
// if deps were provided, they should override the defaults,
|
|
398
|
-
// or things that we created in this function
|
|
399
435
|
...deps,
|
|
400
436
|
epochCache,
|
|
401
437
|
l1TxUtils,
|
|
@@ -416,6 +452,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
416
452
|
if (!options.dontStartSequencer && sequencer) {
|
|
417
453
|
await sequencer.start();
|
|
418
454
|
log.verbose(`Sequencer started`);
|
|
455
|
+
} else if (sequencer) {
|
|
456
|
+
log.warn(`Sequencer created but not started`);
|
|
419
457
|
}
|
|
420
458
|
|
|
421
459
|
return new AztecNodeService(
|
|
@@ -473,6 +511,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
473
511
|
return Promise.resolve(this.p2pClient.getEnr()?.encodeTxt());
|
|
474
512
|
}
|
|
475
513
|
|
|
514
|
+
public async getAllowedPublicSetup(): Promise<AllowedElement[]> {
|
|
515
|
+
return this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
|
|
516
|
+
}
|
|
517
|
+
|
|
476
518
|
/**
|
|
477
519
|
* Method to determine if the node is ready to accept transactions.
|
|
478
520
|
* @returns - Flag indicating the readiness for tx submission.
|
|
@@ -508,8 +550,29 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
508
550
|
* @param number - The block number being requested.
|
|
509
551
|
* @returns The requested block.
|
|
510
552
|
*/
|
|
511
|
-
public async getBlock(number:
|
|
512
|
-
|
|
553
|
+
public async getBlock(number: L2BlockNumber): Promise<L2Block | undefined> {
|
|
554
|
+
const blockNumber = number === 'latest' ? await this.getBlockNumber() : number;
|
|
555
|
+
return await this.blockSource.getBlock(blockNumber);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Get a block specified by its hash.
|
|
560
|
+
* @param blockHash - The block hash being requested.
|
|
561
|
+
* @returns The requested block.
|
|
562
|
+
*/
|
|
563
|
+
public async getBlockByHash(blockHash: Fr): Promise<L2Block | undefined> {
|
|
564
|
+
const publishedBlock = await this.blockSource.getPublishedBlockByHash(blockHash);
|
|
565
|
+
return publishedBlock?.block;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Get a block specified by its archive root.
|
|
570
|
+
* @param archive - The archive root being requested.
|
|
571
|
+
* @returns The requested block.
|
|
572
|
+
*/
|
|
573
|
+
public async getBlockByArchive(archive: Fr): Promise<L2Block | undefined> {
|
|
574
|
+
const publishedBlock = await this.blockSource.getPublishedBlockByArchive(archive);
|
|
575
|
+
return publishedBlock?.block;
|
|
513
576
|
}
|
|
514
577
|
|
|
515
578
|
/**
|
|
@@ -858,13 +921,19 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
858
921
|
return [witness.index, witness.path];
|
|
859
922
|
}
|
|
860
923
|
|
|
924
|
+
public async getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise<number | undefined> {
|
|
925
|
+
const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
|
|
926
|
+
return messageIndex ? InboxLeaf.l2BlockFromIndex(messageIndex) : undefined;
|
|
927
|
+
}
|
|
928
|
+
|
|
861
929
|
/**
|
|
862
930
|
* Returns whether an L1 to L2 message is synced by archiver and if it's ready to be included in a block.
|
|
863
931
|
* @param l1ToL2Message - The L1 to L2 message to check.
|
|
864
932
|
* @returns Whether the message is synced and ready to be included in a block.
|
|
865
933
|
*/
|
|
866
934
|
public async isL1ToL2MessageSynced(l1ToL2Message: Fr): Promise<boolean> {
|
|
867
|
-
|
|
935
|
+
const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
|
|
936
|
+
return messageIndex !== undefined;
|
|
868
937
|
}
|
|
869
938
|
|
|
870
939
|
/**
|
|
@@ -1014,6 +1083,24 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
1014
1083
|
: this.blockSource.getBlockHeader(blockNumber);
|
|
1015
1084
|
}
|
|
1016
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* Get a block header specified by its hash.
|
|
1088
|
+
* @param blockHash - The block hash being requested.
|
|
1089
|
+
* @returns The requested block header.
|
|
1090
|
+
*/
|
|
1091
|
+
public async getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined> {
|
|
1092
|
+
return await this.blockSource.getBlockHeaderByHash(blockHash);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* Get a block header specified by its archive root.
|
|
1097
|
+
* @param archive - The archive root being requested.
|
|
1098
|
+
* @returns The requested block header.
|
|
1099
|
+
*/
|
|
1100
|
+
public async getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
|
|
1101
|
+
return await this.blockSource.getBlockHeaderByArchive(archive);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1017
1104
|
/**
|
|
1018
1105
|
* Simulates the public part of a transaction with the current state.
|
|
1019
1106
|
* @param tx - The transaction to simulate.
|
|
@@ -1062,12 +1149,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
1062
1149
|
|
|
1063
1150
|
const merkleTreeFork = await this.worldStateSynchronizer.fork();
|
|
1064
1151
|
try {
|
|
1065
|
-
const processor = publicProcessorFactory.create(
|
|
1066
|
-
merkleTreeFork,
|
|
1067
|
-
newGlobalVariables,
|
|
1152
|
+
const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, {
|
|
1068
1153
|
skipFeeEnforcement,
|
|
1069
|
-
|
|
1070
|
-
|
|
1154
|
+
clientInitiatedSimulation: true,
|
|
1155
|
+
maxDebugLogMemoryReads: this.config.rpcSimulatePublicMaxDebugLogMemoryReads,
|
|
1156
|
+
});
|
|
1071
1157
|
|
|
1072
1158
|
// REFACTOR: Consider merging ProcessReturnValues into ProcessedTx
|
|
1073
1159
|
const [processedTxs, failedTxs, _usedTxs, returns] = await processor.process([tx]);
|
|
@@ -1122,12 +1208,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
|
|
|
1122
1208
|
|
|
1123
1209
|
public async setConfig(config: Partial<AztecNodeAdminConfig>): Promise<void> {
|
|
1124
1210
|
const newConfig = { ...this.config, ...config };
|
|
1125
|
-
this.sequencer?.
|
|
1211
|
+
this.sequencer?.updateConfig(config);
|
|
1126
1212
|
this.slasherClient?.updateConfig(config);
|
|
1127
1213
|
this.validatorsSentinel?.updateConfig(config);
|
|
1128
|
-
// this.blockBuilder.updateConfig(config); // TODO: Spyros has a PR to add the builder to `this`, so we can do this
|
|
1129
1214
|
await this.p2pClient.updateP2PConfig(config);
|
|
1130
|
-
|
|
1215
|
+
const archiver = this.blockSource as Archiver;
|
|
1216
|
+
if ('updateConfig' in archiver) {
|
|
1217
|
+
archiver.updateConfig(config);
|
|
1218
|
+
}
|
|
1131
1219
|
if (newConfig.realProofs !== this.config.realProofs) {
|
|
1132
1220
|
this.proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier();
|
|
1133
1221
|
}
|
package/src/sentinel/config.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type ConfigMappingsType, booleanConfigHelper, numberConfigHelper } from
|
|
|
2
2
|
|
|
3
3
|
export type SentinelConfig = {
|
|
4
4
|
sentinelHistoryLengthInEpochs: number;
|
|
5
|
+
sentinelHistoricProvenPerformanceLengthInEpochs: number;
|
|
5
6
|
sentinelEnabled: boolean;
|
|
6
7
|
};
|
|
7
8
|
|
|
@@ -11,6 +12,23 @@ export const sentinelConfigMappings: ConfigMappingsType<SentinelConfig> = {
|
|
|
11
12
|
env: 'SENTINEL_HISTORY_LENGTH_IN_EPOCHS',
|
|
12
13
|
...numberConfigHelper(24),
|
|
13
14
|
},
|
|
15
|
+
/**
|
|
16
|
+
* The number of L2 epochs kept of proven performance history for each validator.
|
|
17
|
+
* This value must be large enough so that we have proven performance for every validator
|
|
18
|
+
* for at least slashInactivityConsecutiveEpochThreshold. Assuming this value is 3,
|
|
19
|
+
* and the committee size is 48, and we have 10k validators, then we pick 48 out of 10k each draw.
|
|
20
|
+
* For any fixed element, per-draw prob = 48/10000 = 0.0048.
|
|
21
|
+
* After n draws, count ~ Binomial(n, 0.0048). We want P(X >= 3).
|
|
22
|
+
* Results (exact binomial):
|
|
23
|
+
* - 90% chance: n = 1108
|
|
24
|
+
* - 95% chance: n = 1310
|
|
25
|
+
* - 99% chance: n = 1749
|
|
26
|
+
*/
|
|
27
|
+
sentinelHistoricProvenPerformanceLengthInEpochs: {
|
|
28
|
+
description: 'The number of L2 epochs kept of proven performance history for each validator.',
|
|
29
|
+
env: 'SENTINEL_HISTORIC_PROVEN_PERFORMANCE_LENGTH_IN_EPOCHS',
|
|
30
|
+
...numberConfigHelper(2000),
|
|
31
|
+
},
|
|
14
32
|
sentinelEnabled: {
|
|
15
33
|
description: 'Whether the sentinel is enabled or not.',
|
|
16
34
|
env: 'SENTINEL_ENABLED',
|
package/src/sentinel/factory.ts
CHANGED
|
@@ -27,6 +27,10 @@ export async function createSentinel(
|
|
|
27
27
|
createLogger('node:sentinel:lmdb'),
|
|
28
28
|
);
|
|
29
29
|
const storeHistoryLength = config.sentinelHistoryLengthInEpochs * epochCache.getL1Constants().epochDuration;
|
|
30
|
-
const
|
|
30
|
+
const storeHistoricProvenPerformanceLength = config.sentinelHistoricProvenPerformanceLengthInEpochs;
|
|
31
|
+
const sentinelStore = new SentinelStore(kvStore, {
|
|
32
|
+
historyLength: storeHistoryLength,
|
|
33
|
+
historicProvenPerformanceLength: storeHistoricProvenPerformanceLength,
|
|
34
|
+
});
|
|
31
35
|
return new Sentinel(epochCache, archiver, p2p, sentinelStore, config, logger);
|
|
32
36
|
}
|
package/src/sentinel/sentinel.ts
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import { countWhile, filterAsync } from '@aztec/foundation/collection';
|
|
2
|
+
import { countWhile, filterAsync, fromEntries, getEntries, mapValues } from '@aztec/foundation/collection';
|
|
3
3
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
4
|
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 {
|
|
8
|
+
import {
|
|
9
|
+
OffenseType,
|
|
10
|
+
WANT_TO_SLASH_EVENT,
|
|
11
|
+
type WantToSlashArgs,
|
|
12
|
+
type Watcher,
|
|
13
|
+
type WatcherEmitter,
|
|
14
|
+
} from '@aztec/slasher';
|
|
9
15
|
import type { SlasherConfig } from '@aztec/slasher/config';
|
|
10
16
|
import {
|
|
11
17
|
type L2BlockSource,
|
|
12
18
|
L2BlockStream,
|
|
13
19
|
type L2BlockStreamEvent,
|
|
14
20
|
type L2BlockStreamEventHandler,
|
|
15
|
-
|
|
21
|
+
getAttestationInfoFromPublishedL2Block,
|
|
16
22
|
} from '@aztec/stdlib/block';
|
|
17
|
-
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
23
|
+
import { getEpochAtSlot, getSlotRangeForEpoch, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
18
24
|
import type {
|
|
19
25
|
SingleValidatorStats,
|
|
20
26
|
ValidatorStats,
|
|
@@ -85,7 +91,9 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
85
91
|
this.slotNumberToBlock.set(block.block.header.getSlot(), {
|
|
86
92
|
blockNumber: block.block.number,
|
|
87
93
|
archive: block.block.archive.root.toString(),
|
|
88
|
-
attestors:
|
|
94
|
+
attestors: getAttestationInfoFromPublishedL2Block(block)
|
|
95
|
+
.filter(a => a.status === 'recovered-from-signature')
|
|
96
|
+
.map(a => a.address!),
|
|
89
97
|
});
|
|
90
98
|
}
|
|
91
99
|
|
|
@@ -115,53 +123,38 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
115
123
|
return;
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
|
|
126
|
+
// TODO(palla/slash): We should only be computing proven performance if this is
|
|
127
|
+
// a full proof epoch and not a partial one, otherwise we'll end up with skewed stats.
|
|
128
|
+
const epoch = getEpochAtSlot(block.header.getSlot(), this.epochCache.getL1Constants());
|
|
119
129
|
this.logger.debug(`Computing proven performance for epoch ${epoch}`);
|
|
120
130
|
const performance = await this.computeProvenPerformance(epoch);
|
|
121
131
|
this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
|
|
122
132
|
|
|
123
|
-
await this.updateProvenPerformance(epoch, performance);
|
|
133
|
+
await this.store.updateProvenPerformance(epoch, performance);
|
|
124
134
|
await this.handleProvenPerformance(epoch, performance);
|
|
125
135
|
}
|
|
126
136
|
|
|
127
|
-
protected async computeProvenPerformance(epoch: bigint) {
|
|
128
|
-
const
|
|
129
|
-
const provenSlots = headers.map(h => h.getSlot());
|
|
130
|
-
const fromSlot = provenSlots[0];
|
|
131
|
-
const toSlot = provenSlots[provenSlots.length - 1];
|
|
137
|
+
protected async computeProvenPerformance(epoch: bigint): Promise<ValidatorsEpochPerformance> {
|
|
138
|
+
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
|
|
132
139
|
const { committee } = await this.epochCache.getCommittee(fromSlot);
|
|
133
140
|
if (!committee) {
|
|
134
141
|
this.logger.trace(`No committee found for slot ${fromSlot}`);
|
|
135
142
|
return {};
|
|
136
143
|
}
|
|
137
|
-
const stats = await this.computeStats({ fromSlot, toSlot });
|
|
138
|
-
this.logger.debug(`Stats for epoch ${epoch}`, stats);
|
|
139
|
-
|
|
140
|
-
const performance: ValidatorsEpochPerformance = {};
|
|
141
|
-
for (const validator of Object.keys(stats.stats)) {
|
|
142
|
-
let address;
|
|
143
|
-
try {
|
|
144
|
-
address = EthAddress.fromString(validator);
|
|
145
|
-
} catch (e) {
|
|
146
|
-
this.logger.error(`Invalid validator address ${validator}`, e);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (!committee.find(v => v.equals(address))) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
let missed = 0;
|
|
153
|
-
for (const history of stats.stats[validator].history) {
|
|
154
|
-
if (provenSlots.includes(history.slot) && history.status === 'attestation-missed') {
|
|
155
|
-
missed++;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
performance[address.toString()] = { missed, total: provenSlots.length };
|
|
159
|
-
}
|
|
160
|
-
return performance;
|
|
161
|
-
}
|
|
162
144
|
|
|
163
|
-
|
|
164
|
-
|
|
145
|
+
const stats = await this.computeStats({ fromSlot, toSlot, validators: committee });
|
|
146
|
+
this.logger.debug(`Stats for epoch ${epoch}`, { ...stats, fromSlot, toSlot, epoch });
|
|
147
|
+
|
|
148
|
+
// Note that we are NOT using the total slots in the epoch as `total` here, since we only
|
|
149
|
+
// compute missed attestations over the blocks that had a proposal in them. So, let's say
|
|
150
|
+
// we have an epoch with 10 slots, but only 5 had a block proposal. A validator that was
|
|
151
|
+
// offline, assuming they were not picked as proposer, will then be reported as having missed
|
|
152
|
+
// 5/5 attestations. If we used the total, they'd be reported as 5/10, which would probably
|
|
153
|
+
// allow them to avoid being slashed.
|
|
154
|
+
return mapValues(stats.stats, stat => ({
|
|
155
|
+
missed: stat.missedAttestations.count + stat.missedProposals.count,
|
|
156
|
+
total: stat.missedAttestations.total + stat.missedProposals.total,
|
|
157
|
+
}));
|
|
165
158
|
}
|
|
166
159
|
|
|
167
160
|
/**
|
|
@@ -199,11 +192,13 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
199
192
|
}
|
|
200
193
|
|
|
201
194
|
protected async handleProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
195
|
+
if (this.config.slashInactivityPenalty === 0n) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const inactiveValidators = getEntries(performance)
|
|
200
|
+
.filter(([_, { missed, total }]) => missed / total >= this.config.slashInactivityTargetPercentage)
|
|
201
|
+
.map(([address]) => address);
|
|
207
202
|
|
|
208
203
|
this.logger.debug(`Found ${inactiveValidators.length} inactive validators in epoch ${epoch}`, {
|
|
209
204
|
inactiveValidators,
|
|
@@ -216,7 +211,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
216
211
|
this.checkPastInactivity(EthAddress.fromString(address), epoch, epochThreshold - 1),
|
|
217
212
|
);
|
|
218
213
|
|
|
219
|
-
const args = criminals.map(address => ({
|
|
214
|
+
const args: WantToSlashArgs[] = criminals.map(address => ({
|
|
220
215
|
validator: EthAddress.fromString(address),
|
|
221
216
|
amount: this.config.slashInactivityPenalty,
|
|
222
217
|
offenseType: OffenseType.INACTIVITY,
|
|
@@ -224,7 +219,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
224
219
|
}));
|
|
225
220
|
|
|
226
221
|
if (criminals.length > 0) {
|
|
227
|
-
this.logger.
|
|
222
|
+
this.logger.verbose(
|
|
228
223
|
`Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`,
|
|
229
224
|
{ ...args, epochThreshold },
|
|
230
225
|
);
|
|
@@ -279,7 +274,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
279
274
|
}
|
|
280
275
|
|
|
281
276
|
const archiverSlot = await this.archiver.getL2SlotNumber();
|
|
282
|
-
if (archiverSlot < targetSlot) {
|
|
277
|
+
if (archiverSlot === undefined || archiverSlot < targetSlot) {
|
|
283
278
|
this.logger.debug(`Waiting for archiver to sync with L2 slot ${targetSlot}`, { archiverSlot, targetSlot });
|
|
284
279
|
return false;
|
|
285
280
|
}
|
|
@@ -326,15 +321,20 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
326
321
|
// (contains the ones synced from mined blocks, which we may have missed from p2p).
|
|
327
322
|
const block = this.slotNumberToBlock.get(slot);
|
|
328
323
|
const p2pAttested = await this.p2p.getAttestationsForSlot(slot, block?.archive);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
324
|
+
// Filter out attestations with invalid signatures
|
|
325
|
+
const p2pAttestors = p2pAttested.map(a => a.getSender()).filter((s): s is EthAddress => s !== undefined);
|
|
326
|
+
const attestors = new Set(
|
|
327
|
+
[...p2pAttestors.map(a => a.toString()), ...(block?.attestors.map(a => a.toString()) ?? [])].filter(
|
|
328
|
+
addr => proposer.toString() !== addr, // Exclude the proposer from the attestors
|
|
329
|
+
),
|
|
330
|
+
);
|
|
333
331
|
|
|
334
|
-
// We assume that there was a block proposal if at least one of the validators attested to it.
|
|
332
|
+
// We assume that there was a block proposal if at least one of the validators (other than the proposer) attested to it.
|
|
335
333
|
// It could be the case that every single validator failed, and we could differentiate it by having
|
|
336
334
|
// this node re-execute every block proposal it sees and storing it in the attestation pool.
|
|
337
335
|
// But we'll leave that corner case out to reduce pressure on the node.
|
|
336
|
+
// TODO(palla/slash): This breaks if a given node has more than one validator in the current committee,
|
|
337
|
+
// since they will attest to their own proposal it even if it's not re-executable.
|
|
338
338
|
const blockStatus = block ? 'mined' : attestors.size > 0 ? 'proposed' : 'missed';
|
|
339
339
|
this.logger.debug(`Block for slot ${slot} was ${blockStatus}`, { ...block, slot });
|
|
340
340
|
|
|
@@ -378,20 +378,24 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
378
378
|
|
|
379
379
|
/** Computes stats to be returned based on stored data. */
|
|
380
380
|
public async computeStats({
|
|
381
|
-
fromSlot
|
|
382
|
-
toSlot
|
|
383
|
-
|
|
384
|
-
|
|
381
|
+
fromSlot,
|
|
382
|
+
toSlot,
|
|
383
|
+
validators,
|
|
384
|
+
}: { fromSlot?: bigint; toSlot?: bigint; validators?: EthAddress[] } = {}): Promise<ValidatorsStats> {
|
|
385
|
+
const histories = validators
|
|
386
|
+
? fromEntries(await Promise.all(validators.map(async v => [v.toString(), await this.store.getHistory(v)])))
|
|
387
|
+
: await this.store.getHistories();
|
|
388
|
+
|
|
385
389
|
const slotNow = this.epochCache.getEpochAndSlotNow().slot;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
390
|
+
fromSlot ??= (this.lastProcessedSlot ?? slotNow) - BigInt(this.store.getHistoryLength());
|
|
391
|
+
toSlot ??= this.lastProcessedSlot ?? slotNow;
|
|
392
|
+
|
|
393
|
+
const stats = mapValues(histories, (history, address) =>
|
|
394
|
+
this.computeStatsForValidator(address, history ?? [], fromSlot, toSlot),
|
|
395
|
+
);
|
|
396
|
+
|
|
393
397
|
return {
|
|
394
|
-
stats
|
|
398
|
+
stats,
|
|
395
399
|
lastProcessedSlot: this.lastProcessedSlot,
|
|
396
400
|
initialSlot: this.initialSlot,
|
|
397
401
|
slotWindow: this.store.getHistoryLength(),
|
|
@@ -447,30 +451,31 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
|
|
|
447
451
|
): ValidatorStats {
|
|
448
452
|
let history = fromSlot ? allHistory.filter(h => h.slot >= fromSlot) : allHistory;
|
|
449
453
|
history = toSlot ? history.filter(h => h.slot <= toSlot) : history;
|
|
454
|
+
const lastProposal = history.filter(h => h.status === 'block-proposed' || h.status === 'block-mined').at(-1);
|
|
455
|
+
const lastAttestation = history.filter(h => h.status === 'attestation-sent').at(-1);
|
|
450
456
|
return {
|
|
451
457
|
address: EthAddress.fromString(address),
|
|
452
|
-
lastProposal: this.computeFromSlot(
|
|
453
|
-
|
|
454
|
-
),
|
|
455
|
-
lastAttestation: this.computeFromSlot(history.filter(h => h.status === 'attestation-sent').at(-1)?.slot),
|
|
458
|
+
lastProposal: this.computeFromSlot(lastProposal?.slot),
|
|
459
|
+
lastAttestation: this.computeFromSlot(lastAttestation?.slot),
|
|
456
460
|
totalSlots: history.length,
|
|
457
|
-
missedProposals: this.computeMissed(history, 'block', 'block-missed'),
|
|
458
|
-
missedAttestations: this.computeMissed(history, 'attestation', 'attestation-missed'),
|
|
461
|
+
missedProposals: this.computeMissed(history, 'block', ['block-missed']),
|
|
462
|
+
missedAttestations: this.computeMissed(history, 'attestation', ['attestation-missed']),
|
|
459
463
|
history,
|
|
460
464
|
};
|
|
461
465
|
}
|
|
462
466
|
|
|
463
467
|
protected computeMissed(
|
|
464
468
|
history: ValidatorStatusHistory,
|
|
465
|
-
computeOverPrefix: ValidatorStatusType,
|
|
466
|
-
filter: ValidatorStatusInSlot,
|
|
469
|
+
computeOverPrefix: ValidatorStatusType | undefined,
|
|
470
|
+
filter: ValidatorStatusInSlot[],
|
|
467
471
|
) {
|
|
468
|
-
const relevantHistory = history.filter(h => h.status.startsWith(computeOverPrefix));
|
|
469
|
-
const filteredHistory = relevantHistory.filter(h => h.status
|
|
472
|
+
const relevantHistory = history.filter(h => !computeOverPrefix || h.status.startsWith(computeOverPrefix));
|
|
473
|
+
const filteredHistory = relevantHistory.filter(h => filter.includes(h.status));
|
|
470
474
|
return {
|
|
471
|
-
currentStreak: countWhile([...relevantHistory].reverse(), h => h.status
|
|
475
|
+
currentStreak: countWhile([...relevantHistory].reverse(), h => filter.includes(h.status)),
|
|
472
476
|
rate: relevantHistory.length === 0 ? undefined : filteredHistory.length / relevantHistory.length,
|
|
473
477
|
count: filteredHistory.length,
|
|
478
|
+
total: relevantHistory.length,
|
|
474
479
|
};
|
|
475
480
|
}
|
|
476
481
|
|