@aztec/sequencer-client 1.2.1 → 2.0.0-nightly.20250814
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/client/sequencer-client.d.ts +0 -2
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +5 -7
- package/dest/config.d.ts +1 -0
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +27 -0
- package/dest/publisher/index.d.ts +1 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -1
- package/dest/publisher/sequencer-publisher.d.ts +48 -33
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +272 -114
- package/dest/sequencer/block_builder.d.ts +2 -7
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +3 -7
- package/dest/sequencer/sequencer.d.ts +27 -15
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +141 -85
- package/dest/sequencer/timetable.d.ts +15 -5
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +25 -12
- package/dest/sequencer/utils.d.ts +1 -11
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +0 -23
- package/package.json +26 -26
- package/src/client/sequencer-client.ts +4 -8
- package/src/config.ts +33 -0
- package/src/publisher/index.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +318 -131
- package/src/sequencer/block_builder.ts +15 -8
- package/src/sequencer/sequencer.ts +217 -87
- package/src/sequencer/timetable.ts +43 -7
- package/src/sequencer/utils.ts +6 -37
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { L2Block } from '@aztec/aztec.js';
|
|
2
2
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
3
3
|
import { FormattedViemError, NoCommitteeError, type ViemPublicClient } from '@aztec/ethereum';
|
|
4
|
-
import {
|
|
5
|
-
import { omit } from '@aztec/foundation/collection';
|
|
4
|
+
import { omit, pick } from '@aztec/foundation/collection';
|
|
6
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
6
|
import { Fr } from '@aztec/foundation/fields';
|
|
8
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -12,7 +11,7 @@ import type { TypedEventEmitter } from '@aztec/foundation/types';
|
|
|
12
11
|
import type { P2P } from '@aztec/p2p';
|
|
13
12
|
import type { SlasherClient } from '@aztec/slasher';
|
|
14
13
|
import { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
15
|
-
import type { CommitteeAttestation, L2BlockSource } from '@aztec/stdlib/block';
|
|
14
|
+
import type { CommitteeAttestation, L2BlockSource, ValidateBlockResult } from '@aztec/stdlib/block';
|
|
16
15
|
import { type L1RollupConstants, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
17
16
|
import { Gas } from '@aztec/stdlib/gas';
|
|
18
17
|
import {
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
} from '@aztec/stdlib/interfaces/server';
|
|
24
23
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
25
24
|
import type { BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
25
|
+
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
26
26
|
import { pickFromSchema } from '@aztec/stdlib/schemas';
|
|
27
27
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
28
28
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
@@ -48,22 +48,37 @@ import type { ValidatorClient } from '@aztec/validator-client';
|
|
|
48
48
|
import EventEmitter from 'node:events';
|
|
49
49
|
|
|
50
50
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
51
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
type Action,
|
|
53
|
+
type InvalidateBlockRequest,
|
|
54
|
+
type SequencerPublisher,
|
|
55
|
+
SignalType,
|
|
56
|
+
} from '../publisher/sequencer-publisher.js';
|
|
52
57
|
import type { SequencerConfig } from './config.js';
|
|
53
58
|
import { SequencerMetrics } from './metrics.js';
|
|
54
59
|
import { SequencerTimetable, SequencerTooSlowError } from './timetable.js';
|
|
55
|
-
import { SequencerState,
|
|
60
|
+
import { SequencerState, type SequencerStateWithSlot } from './utils.js';
|
|
56
61
|
|
|
57
62
|
export { SequencerState };
|
|
58
63
|
|
|
59
64
|
type SequencerRollupConstants = Pick<L1RollupConstants, 'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration'>;
|
|
60
65
|
|
|
61
66
|
export type SequencerEvents = {
|
|
62
|
-
['state-changed']: (args: {
|
|
67
|
+
['state-changed']: (args: {
|
|
68
|
+
oldState: SequencerState;
|
|
69
|
+
newState: SequencerState;
|
|
70
|
+
secondsIntoSlot?: number;
|
|
71
|
+
slotNumber?: bigint;
|
|
72
|
+
}) => void;
|
|
63
73
|
['proposer-rollup-check-failed']: (args: { reason: string }) => void;
|
|
64
74
|
['tx-count-check-failed']: (args: { minTxs: number; availableTxs: number }) => void;
|
|
65
75
|
['block-build-failed']: (args: { reason: string }) => void;
|
|
66
|
-
['block-publish-failed']: (args: {
|
|
76
|
+
['block-publish-failed']: (args: {
|
|
77
|
+
successfulActions?: Action[];
|
|
78
|
+
failedActions?: Action[];
|
|
79
|
+
sentActions?: Action[];
|
|
80
|
+
expiredActions?: Action[];
|
|
81
|
+
}) => void;
|
|
67
82
|
['block-published']: (args: { blockNumber: number; slot: number }) => void;
|
|
68
83
|
};
|
|
69
84
|
|
|
@@ -91,7 +106,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
91
106
|
private metrics: SequencerMetrics;
|
|
92
107
|
private l1Metrics: L1Metrics;
|
|
93
108
|
private lastBlockPublished: L2Block | undefined;
|
|
94
|
-
private isFlushing: boolean = false;
|
|
95
109
|
|
|
96
110
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
|
|
97
111
|
protected timetable!: SequencerTimetable;
|
|
@@ -130,6 +144,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
130
144
|
|
|
131
145
|
// Register the slasher on the publisher to fetch slashing payloads
|
|
132
146
|
this.publisher.registerSlashPayloadGetter(this.slasherClient.getSlashPayload.bind(this.slasherClient));
|
|
147
|
+
|
|
148
|
+
// Initialize config
|
|
149
|
+
this.updateConfig(this.config);
|
|
133
150
|
}
|
|
134
151
|
|
|
135
152
|
get tracer(): Tracer {
|
|
@@ -140,6 +157,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
140
157
|
return this.validatorClient?.getValidatorAddresses();
|
|
141
158
|
}
|
|
142
159
|
|
|
160
|
+
public getConfig() {
|
|
161
|
+
return this.config;
|
|
162
|
+
}
|
|
163
|
+
|
|
143
164
|
/**
|
|
144
165
|
* Updates sequencer config by the defined values in the config on input.
|
|
145
166
|
* @param config - New parameters.
|
|
@@ -195,24 +216,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
195
216
|
|
|
196
217
|
private setTimeTable() {
|
|
197
218
|
this.timetable = new SequencerTimetable(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
219
|
+
{
|
|
220
|
+
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
221
|
+
aztecSlotDuration: this.aztecSlotDuration,
|
|
222
|
+
maxL1TxInclusionTimeIntoSlot: this.maxL1TxInclusionTimeIntoSlot,
|
|
223
|
+
attestationPropagationTime: this.config.attestationPropagationTime,
|
|
224
|
+
enforce: this.enforceTimeTable,
|
|
225
|
+
},
|
|
202
226
|
this.metrics,
|
|
203
227
|
this.log,
|
|
204
228
|
);
|
|
205
|
-
this.log.verbose(`Sequencer timetable updated`, { enforceTimeTable: this.enforceTimeTable });
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
/**
|
|
209
232
|
* Starts the sequencer and moves to IDLE state.
|
|
210
233
|
*/
|
|
211
234
|
public start() {
|
|
212
|
-
this.updateConfig(this.config);
|
|
213
235
|
this.metrics.start();
|
|
214
236
|
this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
|
|
215
|
-
this.setState(SequencerState.IDLE,
|
|
237
|
+
this.setState(SequencerState.IDLE, undefined, { force: true });
|
|
216
238
|
this.runningPromise.start();
|
|
217
239
|
this.l1Metrics.start();
|
|
218
240
|
this.log.info(`Sequencer started with address ${this.publisher.getSenderAddress().toString()}`);
|
|
@@ -222,12 +244,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
222
244
|
* Stops the sequencer from processing txs and moves to STOPPED state.
|
|
223
245
|
*/
|
|
224
246
|
public async stop(): Promise<void> {
|
|
225
|
-
this.log.
|
|
247
|
+
this.log.info(`Stopping sequencer`);
|
|
226
248
|
this.metrics.stop();
|
|
227
249
|
await this.validatorClient?.stop();
|
|
228
250
|
await this.runningPromise?.stop();
|
|
229
251
|
this.publisher.interrupt();
|
|
230
|
-
this.setState(SequencerState.STOPPED,
|
|
252
|
+
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
231
253
|
this.l1Metrics.stop();
|
|
232
254
|
this.log.info('Stopped sequencer');
|
|
233
255
|
}
|
|
@@ -239,7 +261,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
239
261
|
this.log.info('Restarting sequencer');
|
|
240
262
|
this.publisher.restart();
|
|
241
263
|
this.runningPromise!.start();
|
|
242
|
-
this.setState(SequencerState.IDLE,
|
|
264
|
+
this.setState(SequencerState.IDLE, undefined, { force: true });
|
|
243
265
|
}
|
|
244
266
|
|
|
245
267
|
/**
|
|
@@ -250,11 +272,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
250
272
|
return { state: this.state };
|
|
251
273
|
}
|
|
252
274
|
|
|
253
|
-
/** Forces the sequencer to bypass all time and tx count checks for the next block and build anyway. */
|
|
254
|
-
public flush() {
|
|
255
|
-
this.isFlushing = true;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
275
|
/**
|
|
259
276
|
* @notice Performs most of the sequencer duties:
|
|
260
277
|
* - Checks if we are up to date
|
|
@@ -264,7 +281,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
264
281
|
* - If our block for some reason is not included, revert the state
|
|
265
282
|
*/
|
|
266
283
|
protected async doRealWork() {
|
|
267
|
-
this.setState(SequencerState.SYNCHRONIZING,
|
|
284
|
+
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
268
285
|
|
|
269
286
|
// Check all components are synced to latest as seen by the archiver
|
|
270
287
|
const syncedTo = await this.getChainTip();
|
|
@@ -274,7 +291,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
274
291
|
return;
|
|
275
292
|
}
|
|
276
293
|
|
|
277
|
-
this.setState(SequencerState.PROPOSER_CHECK,
|
|
294
|
+
this.setState(SequencerState.PROPOSER_CHECK, undefined);
|
|
278
295
|
|
|
279
296
|
const chainTipArchive = syncedTo.archive;
|
|
280
297
|
const newBlockNumber = syncedTo.blockNumber + 1;
|
|
@@ -292,6 +309,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
292
309
|
nextL2Slot: slot,
|
|
293
310
|
nextL2SlotTs: ts,
|
|
294
311
|
l1SlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
312
|
+
newBlockNumber,
|
|
313
|
+
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
295
314
|
};
|
|
296
315
|
|
|
297
316
|
if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
|
|
@@ -320,35 +339,47 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
320
339
|
return;
|
|
321
340
|
}
|
|
322
341
|
|
|
342
|
+
// Check that we are a proposer for the next slot
|
|
323
343
|
let proposerInNextSlot: EthAddress | undefined;
|
|
324
344
|
try {
|
|
325
|
-
// Check that we are a proposer for the next slot
|
|
326
345
|
proposerInNextSlot = await this.publisher.epochCache.getProposerAttesterAddressInNextSlot();
|
|
327
346
|
} catch (e) {
|
|
328
347
|
if (e instanceof NoCommitteeError) {
|
|
329
|
-
this.log.warn(
|
|
348
|
+
this.log.warn(
|
|
349
|
+
`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`,
|
|
350
|
+
);
|
|
330
351
|
return;
|
|
331
352
|
}
|
|
332
353
|
}
|
|
333
|
-
const validatorAddresses = this.validatorClient!.getValidatorAddresses();
|
|
334
354
|
|
|
335
355
|
// If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
|
|
336
|
-
// If the committee is defined and not empty, but none of our validators are the proposer,
|
|
337
|
-
|
|
356
|
+
// If the committee is defined and not empty, but none of our validators are the proposer, then stop.
|
|
357
|
+
const validatorAddresses = this.validatorClient!.getValidatorAddresses();
|
|
338
358
|
if (proposerInNextSlot !== undefined && !validatorAddresses.some(addr => addr.equals(proposerInNextSlot))) {
|
|
339
359
|
this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
|
|
340
360
|
us: validatorAddresses,
|
|
341
361
|
proposer: proposerInNextSlot,
|
|
342
362
|
...syncLogData,
|
|
343
363
|
});
|
|
364
|
+
// If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
|
|
365
|
+
if (!syncedTo.pendingChainValidationStatus.valid) {
|
|
366
|
+
await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses);
|
|
367
|
+
}
|
|
344
368
|
return;
|
|
345
369
|
}
|
|
346
370
|
|
|
347
|
-
//
|
|
348
|
-
|
|
371
|
+
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
372
|
+
const invalidateBlock = await this.publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
|
|
373
|
+
|
|
374
|
+
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
375
|
+
// if all the previous checks are good, but we do it just in case.
|
|
349
376
|
const proposerAddress = proposerInNextSlot ?? EthAddress.ZERO;
|
|
377
|
+
const canProposeCheck = await this.publisher.canProposeAtNextEthBlock(
|
|
378
|
+
chainTipArchive,
|
|
379
|
+
proposerAddress,
|
|
380
|
+
invalidateBlock,
|
|
381
|
+
);
|
|
350
382
|
|
|
351
|
-
const canProposeCheck = await this.publisher.canProposeAtNextEthBlock(chainTipArchive.toBuffer(), proposerAddress);
|
|
352
383
|
if (canProposeCheck === undefined) {
|
|
353
384
|
this.log.warn(
|
|
354
385
|
`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`,
|
|
@@ -373,7 +404,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
373
404
|
}
|
|
374
405
|
|
|
375
406
|
this.log.debug(
|
|
376
|
-
|
|
407
|
+
`Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
|
|
377
408
|
{ ...syncLogData, validatorAddresses },
|
|
378
409
|
);
|
|
379
410
|
|
|
@@ -384,22 +415,26 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
384
415
|
slot,
|
|
385
416
|
);
|
|
386
417
|
|
|
387
|
-
const enqueueGovernanceVotePromise = this.publisher.
|
|
418
|
+
const enqueueGovernanceVotePromise = this.publisher.enqueueCastSignal(
|
|
388
419
|
slot,
|
|
389
420
|
newGlobalVariables.timestamp,
|
|
390
|
-
|
|
421
|
+
SignalType.GOVERNANCE,
|
|
391
422
|
proposerAddress,
|
|
392
|
-
msg => this.validatorClient!.signWithAddress(proposerAddress,
|
|
423
|
+
msg => this.validatorClient!.signWithAddress(proposerAddress, msg).then(s => s.toString()),
|
|
393
424
|
);
|
|
394
425
|
|
|
395
|
-
const enqueueSlashingVotePromise = this.publisher.
|
|
426
|
+
const enqueueSlashingVotePromise = this.publisher.enqueueCastSignal(
|
|
396
427
|
slot,
|
|
397
428
|
newGlobalVariables.timestamp,
|
|
398
|
-
|
|
429
|
+
SignalType.SLASHING,
|
|
399
430
|
proposerAddress,
|
|
400
|
-
msg => this.validatorClient!.signWithAddress(proposerAddress,
|
|
431
|
+
msg => this.validatorClient!.signWithAddress(proposerAddress, msg).then(s => s.toString()),
|
|
401
432
|
);
|
|
402
433
|
|
|
434
|
+
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
435
|
+
this.publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
436
|
+
}
|
|
437
|
+
|
|
403
438
|
this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
|
|
404
439
|
this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
|
|
405
440
|
proposer: proposerInNextSlot?.toString(),
|
|
@@ -418,11 +453,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
418
453
|
totalManaUsed: Fr.ZERO,
|
|
419
454
|
});
|
|
420
455
|
|
|
421
|
-
let finishedFlushing = false;
|
|
422
456
|
let block: L2Block | undefined;
|
|
423
457
|
|
|
424
458
|
const pendingTxCount = await this.p2pClient.getPendingTxCount();
|
|
425
|
-
if (pendingTxCount >= this.minTxsPerBlock
|
|
459
|
+
if (pendingTxCount >= this.minTxsPerBlock) {
|
|
426
460
|
// We don't fetch exactly maxTxsPerBlock txs here because we may not need all of them if we hit a limit before,
|
|
427
461
|
// and also we may need to fetch more if we don't have enough valid txs.
|
|
428
462
|
const pendingTxs = this.p2pClient.iteratePendingTxs();
|
|
@@ -432,6 +466,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
432
466
|
proposalHeader,
|
|
433
467
|
newGlobalVariables,
|
|
434
468
|
proposerInNextSlot,
|
|
469
|
+
invalidateBlock,
|
|
435
470
|
);
|
|
436
471
|
} catch (err: any) {
|
|
437
472
|
this.emit('block-build-failed', { reason: err.message });
|
|
@@ -440,8 +475,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
440
475
|
} else {
|
|
441
476
|
this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
|
|
442
477
|
}
|
|
443
|
-
} finally {
|
|
444
|
-
finishedFlushing = true;
|
|
445
478
|
}
|
|
446
479
|
} else {
|
|
447
480
|
this.log.verbose(
|
|
@@ -459,22 +492,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
459
492
|
});
|
|
460
493
|
|
|
461
494
|
const l1Response = await this.publisher.sendRequests();
|
|
462
|
-
const proposedBlock = l1Response?.
|
|
495
|
+
const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
|
|
463
496
|
if (proposedBlock) {
|
|
464
497
|
this.lastBlockPublished = block;
|
|
465
498
|
this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
|
|
466
499
|
this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString());
|
|
467
|
-
if (finishedFlushing) {
|
|
468
|
-
this.isFlushing = false;
|
|
469
|
-
}
|
|
470
500
|
} else if (block) {
|
|
471
|
-
this.emit('block-publish-failed', {
|
|
472
|
-
validActions: l1Response?.validActions,
|
|
473
|
-
expiredActions: l1Response?.expiredActions,
|
|
474
|
-
});
|
|
501
|
+
this.emit('block-publish-failed', l1Response ?? {});
|
|
475
502
|
}
|
|
476
503
|
|
|
477
|
-
this.setState(SequencerState.IDLE,
|
|
504
|
+
this.setState(SequencerState.IDLE, undefined);
|
|
478
505
|
}
|
|
479
506
|
|
|
480
507
|
@trackSpan('Sequencer.work')
|
|
@@ -489,28 +516,40 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
489
516
|
throw err;
|
|
490
517
|
}
|
|
491
518
|
} finally {
|
|
492
|
-
this.setState(SequencerState.IDLE,
|
|
519
|
+
this.setState(SequencerState.IDLE, undefined);
|
|
493
520
|
}
|
|
494
521
|
}
|
|
495
522
|
|
|
496
523
|
/**
|
|
497
524
|
* Sets the sequencer state and checks if we have enough time left in the slot to transition to the new state.
|
|
498
525
|
* @param proposedState - The new state to transition to.
|
|
499
|
-
* @param
|
|
526
|
+
* @param slotNumber - The current slot number.
|
|
500
527
|
* @param force - Whether to force the transition even if the sequencer is stopped.
|
|
501
|
-
*
|
|
502
|
-
* @dev If the `currentSlotNumber` doesn't matter (e.g. transitioning to IDLE), pass in `0n`;
|
|
503
|
-
* it is only used to check if we have enough time left in the slot to transition to the new state.
|
|
504
528
|
*/
|
|
505
|
-
setState(proposedState:
|
|
506
|
-
|
|
529
|
+
setState(proposedState: SequencerStateWithSlot, slotNumber: bigint, opts?: { force?: boolean }): void;
|
|
530
|
+
setState(
|
|
531
|
+
proposedState: Exclude<SequencerState, SequencerStateWithSlot>,
|
|
532
|
+
slotNumber?: undefined,
|
|
533
|
+
opts?: { force?: boolean },
|
|
534
|
+
): void;
|
|
535
|
+
setState(proposedState: SequencerState, slotNumber: bigint | undefined, opts: { force?: boolean } = {}): void {
|
|
536
|
+
if (this.state === SequencerState.STOPPED && !opts.force) {
|
|
507
537
|
this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
|
|
508
538
|
return;
|
|
509
539
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
540
|
+
let secondsIntoSlot = undefined;
|
|
541
|
+
if (slotNumber !== undefined) {
|
|
542
|
+
secondsIntoSlot = this.getSecondsIntoSlot(slotNumber);
|
|
543
|
+
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
this.log.debug(`Transitioning from ${this.state} to ${proposedState}`, { slotNumber, secondsIntoSlot });
|
|
547
|
+
this.emit('state-changed', {
|
|
548
|
+
oldState: this.state,
|
|
549
|
+
newState: proposedState,
|
|
550
|
+
secondsIntoSlot,
|
|
551
|
+
slotNumber,
|
|
552
|
+
});
|
|
514
553
|
this.state = proposedState;
|
|
515
554
|
}
|
|
516
555
|
|
|
@@ -519,19 +558,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
519
558
|
return;
|
|
520
559
|
}
|
|
521
560
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
522
|
-
const failedTxHashes =
|
|
561
|
+
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
523
562
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
524
563
|
await this.p2pClient.deleteTxs(failedTxHashes);
|
|
525
564
|
}
|
|
526
565
|
|
|
527
|
-
protected
|
|
566
|
+
protected getBlockBuilderOptions(slot: number): PublicProcessorLimits {
|
|
528
567
|
// Deadline for processing depends on whether we're proposing a block
|
|
529
568
|
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
530
569
|
const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
|
|
531
570
|
|
|
532
571
|
// Deadline is only set if enforceTimeTable is enabled.
|
|
533
572
|
const deadline = this.enforceTimeTable
|
|
534
|
-
? new Date((this.
|
|
573
|
+
? new Date((this.getSlotStartBuildTimestamp(slot) + processingEndTimeWithinSlot) * 1000)
|
|
535
574
|
: undefined;
|
|
536
575
|
return {
|
|
537
576
|
maxTransactions: this.maxTxsPerBlock,
|
|
@@ -560,8 +599,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
560
599
|
proposalHeader: ProposedBlockHeader,
|
|
561
600
|
newGlobalVariables: GlobalVariables,
|
|
562
601
|
proposerAddress: EthAddress | undefined,
|
|
602
|
+
invalidateBlock: InvalidateBlockRequest | undefined,
|
|
563
603
|
): Promise<L2Block> {
|
|
564
|
-
await this.publisher.validateBlockHeader(proposalHeader);
|
|
604
|
+
await this.publisher.validateBlockHeader(proposalHeader, invalidateBlock);
|
|
565
605
|
|
|
566
606
|
const blockNumber = newGlobalVariables.blockNumber;
|
|
567
607
|
const slot = proposalHeader.slotNumber.toBigInt();
|
|
@@ -572,7 +612,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
572
612
|
this.setState(SequencerState.CREATING_BLOCK, slot);
|
|
573
613
|
|
|
574
614
|
try {
|
|
575
|
-
const blockBuilderOptions = this.
|
|
615
|
+
const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
|
|
576
616
|
const buildBlockRes = await this.blockBuilder.buildBlock(
|
|
577
617
|
pendingTxs,
|
|
578
618
|
l1ToL2Messages,
|
|
@@ -584,8 +624,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
584
624
|
const blockBuildDuration = workTimer.ms();
|
|
585
625
|
await this.dropFailedTxsFromP2P(failedTxs);
|
|
586
626
|
|
|
587
|
-
const minTxsPerBlock = this.
|
|
588
|
-
|
|
627
|
+
const minTxsPerBlock = this.minTxsPerBlock;
|
|
589
628
|
if (numTxs < minTxsPerBlock) {
|
|
590
629
|
this.log.warn(
|
|
591
630
|
`Block ${blockNumber} has too few txs to be proposed (got ${numTxs} but required ${minTxsPerBlock})`,
|
|
@@ -596,7 +635,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
596
635
|
|
|
597
636
|
// TODO(@PhilWindle) We should probably periodically check for things like another
|
|
598
637
|
// block being published before ours instead of just waiting on our block
|
|
599
|
-
await this.publisher.validateBlockHeader(block.header.toPropose());
|
|
638
|
+
await this.publisher.validateBlockHeader(block.header.toPropose(), invalidateBlock);
|
|
600
639
|
|
|
601
640
|
const blockStats: L2BlockBuiltStats = {
|
|
602
641
|
eventName: 'l2-block-built',
|
|
@@ -627,7 +666,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
627
666
|
this.log.verbose(`Collected ${attestations.length} attestations`, { blockHash, blockNumber });
|
|
628
667
|
}
|
|
629
668
|
|
|
630
|
-
await this.enqueuePublishL2Block(block, attestations, txHashes);
|
|
669
|
+
await this.enqueuePublishL2Block(block, attestations, txHashes, invalidateBlock);
|
|
631
670
|
this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
|
|
632
671
|
return block;
|
|
633
672
|
} catch (err) {
|
|
@@ -646,8 +685,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
646
685
|
txs: Tx[],
|
|
647
686
|
proposerAddress: EthAddress | undefined,
|
|
648
687
|
): Promise<CommitteeAttestation[] | undefined> {
|
|
649
|
-
|
|
650
|
-
const committee = await this.publisher.getCurrentEpochCommittee();
|
|
688
|
+
const { committee } = await this.publisher.epochCache.getCommittee(block.header.getSlot());
|
|
651
689
|
|
|
652
690
|
// We checked above that the committee is defined, so this should never happen.
|
|
653
691
|
if (!committee) {
|
|
@@ -683,9 +721,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
683
721
|
proposerAddress,
|
|
684
722
|
blockProposalOptions,
|
|
685
723
|
);
|
|
724
|
+
|
|
686
725
|
if (!proposal) {
|
|
687
|
-
|
|
688
|
-
|
|
726
|
+
throw new Error(`Failed to create block proposal`);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (this.config.skipCollectingAttestations) {
|
|
730
|
+
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
731
|
+
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
732
|
+
return orderAttestations(attestations ?? [], committee);
|
|
689
733
|
}
|
|
690
734
|
|
|
691
735
|
this.log.debug('Broadcasting block proposal to validators');
|
|
@@ -715,7 +759,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
715
759
|
if (err && err instanceof AttestationTimeoutError) {
|
|
716
760
|
collectedAttestionsCount = err.collectedCount;
|
|
717
761
|
}
|
|
718
|
-
|
|
719
762
|
throw err;
|
|
720
763
|
} finally {
|
|
721
764
|
this.metrics.recordCollectedAttestations(collectedAttestionsCount, timer.ms());
|
|
@@ -731,18 +774,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
731
774
|
}))
|
|
732
775
|
protected async enqueuePublishL2Block(
|
|
733
776
|
block: L2Block,
|
|
734
|
-
attestations
|
|
735
|
-
txHashes
|
|
777
|
+
attestations: CommitteeAttestation[] | undefined,
|
|
778
|
+
txHashes: TxHash[],
|
|
779
|
+
invalidateBlock: InvalidateBlockRequest | undefined,
|
|
736
780
|
): Promise<void> {
|
|
737
781
|
// Publishes new block to the network and awaits the tx to be mined
|
|
738
782
|
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
|
|
739
783
|
|
|
740
784
|
// Time out tx at the end of the slot
|
|
741
785
|
const slot = block.header.globalVariables.slotNumber.toNumber();
|
|
742
|
-
const txTimeoutAt = new Date((this.
|
|
786
|
+
const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
|
|
743
787
|
|
|
744
788
|
const enqueued = await this.publisher.enqueueProposeL2Block(block, attestations, txHashes, {
|
|
745
789
|
txTimeoutAt,
|
|
790
|
+
forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber,
|
|
746
791
|
});
|
|
747
792
|
|
|
748
793
|
if (!enqueued) {
|
|
@@ -756,7 +801,14 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
756
801
|
* @returns Boolean indicating if our dependencies are synced to the latest block.
|
|
757
802
|
*/
|
|
758
803
|
protected async getChainTip(): Promise<
|
|
759
|
-
|
|
804
|
+
| {
|
|
805
|
+
block?: L2Block;
|
|
806
|
+
blockNumber: number;
|
|
807
|
+
archive: Fr;
|
|
808
|
+
l1Timestamp: bigint;
|
|
809
|
+
pendingChainValidationStatus: ValidateBlockResult;
|
|
810
|
+
}
|
|
811
|
+
| undefined
|
|
760
812
|
> {
|
|
761
813
|
const syncedBlocks = await Promise.all([
|
|
762
814
|
this.worldState.status().then(({ syncSummary }) => ({
|
|
@@ -767,9 +819,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
767
819
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
768
820
|
this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
|
|
769
821
|
this.l2BlockSource.getL1Timestamp(),
|
|
822
|
+
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
770
823
|
] as const);
|
|
771
824
|
|
|
772
|
-
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp] =
|
|
825
|
+
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] =
|
|
826
|
+
syncedBlocks;
|
|
773
827
|
|
|
774
828
|
// The archiver reports 'undefined' hash for the genesis block
|
|
775
829
|
// because it doesn't have access to world state to compute it (facepalm)
|
|
@@ -801,19 +855,95 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
801
855
|
blockNumber: block.number,
|
|
802
856
|
archive: block.archive.root,
|
|
803
857
|
l1Timestamp,
|
|
858
|
+
pendingChainValidationStatus,
|
|
804
859
|
};
|
|
805
860
|
} else {
|
|
806
861
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
807
|
-
return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive, l1Timestamp };
|
|
862
|
+
return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive, l1Timestamp, pendingChainValidationStatus };
|
|
808
863
|
}
|
|
809
864
|
}
|
|
810
865
|
|
|
811
|
-
|
|
812
|
-
|
|
866
|
+
/**
|
|
867
|
+
* Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
|
|
868
|
+
* has been there without being invalidated and whether the sequencer is in the committee or not. We always
|
|
869
|
+
* have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
|
|
870
|
+
* and if they fail, any sequencer will try as well.
|
|
871
|
+
*/
|
|
872
|
+
protected async considerInvalidatingBlock(
|
|
873
|
+
syncedTo: NonNullable<Awaited<ReturnType<Sequencer['getChainTip']>>>,
|
|
874
|
+
currentSlot: bigint,
|
|
875
|
+
ourValidatorAddresses: EthAddress[],
|
|
876
|
+
): Promise<void> {
|
|
877
|
+
const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
|
|
878
|
+
if (pendingChainValidationStatus.valid) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const invalidL1Timestamp = pendingChainValidationStatus.block.l1.timestamp;
|
|
883
|
+
const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidL1Timestamp);
|
|
884
|
+
const invalidBlockNumber = pendingChainValidationStatus.block.block.number;
|
|
885
|
+
|
|
886
|
+
const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
|
|
887
|
+
this.config;
|
|
888
|
+
|
|
889
|
+
const logData = {
|
|
890
|
+
invalidL1Timestamp,
|
|
891
|
+
l1Timestamp,
|
|
892
|
+
invalidBlock: pendingChainValidationStatus.block.block.toBlockInfo(),
|
|
893
|
+
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
894
|
+
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
895
|
+
ourValidatorAddresses,
|
|
896
|
+
currentSlot,
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const inCurrentCommittee = () =>
|
|
900
|
+
this.publisher.epochCache
|
|
901
|
+
.getCommittee(currentSlot)
|
|
902
|
+
.then(c => c?.committee?.some(member => ourValidatorAddresses.some(addr => addr.equals(member))));
|
|
903
|
+
|
|
904
|
+
const invalidateAsCommitteeMember =
|
|
905
|
+
secondsBeforeInvalidatingBlockAsCommitteeMember !== undefined &&
|
|
906
|
+
secondsBeforeInvalidatingBlockAsCommitteeMember > 0 &&
|
|
907
|
+
timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsCommitteeMember &&
|
|
908
|
+
(await inCurrentCommittee());
|
|
909
|
+
|
|
910
|
+
const invalidateAsNonCommitteeMember =
|
|
911
|
+
secondsBeforeInvalidatingBlockAsNonCommitteeMember !== undefined &&
|
|
912
|
+
secondsBeforeInvalidatingBlockAsNonCommitteeMember > 0 &&
|
|
913
|
+
timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsNonCommitteeMember;
|
|
914
|
+
|
|
915
|
+
if (!invalidateAsCommitteeMember && !invalidateAsNonCommitteeMember) {
|
|
916
|
+
this.log.debug(`Not invalidating pending chain`, logData);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const invalidateBlock = await this.publisher.simulateInvalidateBlock(pendingChainValidationStatus);
|
|
921
|
+
if (!invalidateBlock) {
|
|
922
|
+
this.log.warn(`Failed to simulate invalidate block`, logData);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
this.log.info(
|
|
927
|
+
invalidateAsCommitteeMember
|
|
928
|
+
? `Invalidating block ${invalidBlockNumber} as committee member`
|
|
929
|
+
: `Invalidating block ${invalidBlockNumber} as non-committee member`,
|
|
930
|
+
logData,
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
this.publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
934
|
+
await this.publisher.sendRequests();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private getSlotStartBuildTimestamp(slotNumber: number | bigint): number {
|
|
938
|
+
return (
|
|
939
|
+
Number(this.l1Constants.l1GenesisTime) +
|
|
940
|
+
Number(slotNumber) * this.l1Constants.slotDuration -
|
|
941
|
+
this.l1Constants.ethereumSlotDuration
|
|
942
|
+
);
|
|
813
943
|
}
|
|
814
944
|
|
|
815
945
|
private getSecondsIntoSlot(slotNumber: number | bigint): number {
|
|
816
|
-
const slotStartTimestamp = this.
|
|
946
|
+
const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
|
|
817
947
|
return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
|
|
818
948
|
}
|
|
819
949
|
|