@aztec/sequencer-client 3.0.0-devnet.6 → 3.0.0-devnet.6-patch.1
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/index.d.ts +1 -1
- package/dest/client/sequencer-client.d.ts +2 -2
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +6 -2
- package/dest/config.d.ts +3 -2
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +11 -1
- package/dest/global_variable_builder/global_builder.d.ts +5 -7
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +12 -8
- package/dest/global_variable_builder/index.d.ts +1 -1
- package/dest/index.d.ts +1 -1
- package/dest/publisher/config.d.ts +7 -2
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +12 -1
- package/dest/publisher/index.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +3 -2
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.d.ts +40 -31
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +117 -62
- package/dest/sequencer/block_builder.d.ts +4 -3
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +5 -8
- package/dest/sequencer/config.d.ts +2 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/errors.d.ts +1 -1
- package/dest/sequencer/errors.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +1 -1
- package/dest/sequencer/metrics.d.ts +12 -3
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +38 -0
- package/dest/sequencer/sequencer.d.ts +48 -27
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +418 -166
- package/dest/sequencer/timetable.d.ts +3 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/utils.d.ts +1 -1
- package/dest/test/index.d.ts +2 -2
- package/dest/test/index.d.ts.map +1 -1
- package/dest/tx_validator/nullifier_cache.d.ts +1 -1
- package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +1 -1
- package/package.json +31 -30
- package/src/client/sequencer-client.ts +6 -9
- package/src/config.ts +12 -6
- package/src/global_variable_builder/global_builder.ts +19 -17
- package/src/publisher/config.ts +17 -6
- package/src/publisher/sequencer-publisher-factory.ts +4 -2
- package/src/publisher/sequencer-publisher.ts +165 -94
- package/src/sequencer/block_builder.ts +8 -12
- package/src/sequencer/config.ts +1 -1
- package/src/sequencer/metrics.ts +52 -3
- package/src/sequencer/sequencer.ts +480 -198
- package/src/sequencer/timetable.ts +7 -0
- package/src/test/index.ts +1 -1
- package/src/tx_validator/tx_validator_factory.ts +3 -2
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { L2Block } from '@aztec/aztec.js/block';
|
|
2
|
-
import {
|
|
2
|
+
import { getKzg } from '@aztec/blob-lib';
|
|
3
|
+
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
3
4
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
-
import {
|
|
5
|
+
import { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts';
|
|
6
|
+
import { FormattedViemError } from '@aztec/ethereum/utils';
|
|
7
|
+
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
8
|
import { omit, pick } from '@aztec/foundation/collection';
|
|
6
|
-
import { randomInt } from '@aztec/foundation/crypto';
|
|
9
|
+
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
7
11
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
8
12
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
9
|
-
import { Fr } from '@aztec/foundation/fields';
|
|
10
13
|
import { createLogger } from '@aztec/foundation/log';
|
|
11
14
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
12
15
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
@@ -17,6 +20,7 @@ import {
|
|
|
17
20
|
CommitteeAttestation,
|
|
18
21
|
CommitteeAttestationsAndSigners,
|
|
19
22
|
type L2BlockSource,
|
|
23
|
+
MaliciousCommitteeAttestationsAndSigners,
|
|
20
24
|
type ValidateBlockResult,
|
|
21
25
|
} from '@aztec/stdlib/block';
|
|
22
26
|
import { type L1RollupConstants, getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
@@ -60,7 +64,7 @@ export type SequencerEvents = {
|
|
|
60
64
|
oldState: SequencerState;
|
|
61
65
|
newState: SequencerState;
|
|
62
66
|
secondsIntoSlot?: number;
|
|
63
|
-
slotNumber?:
|
|
67
|
+
slotNumber?: SlotNumber;
|
|
64
68
|
}) => void;
|
|
65
69
|
['proposer-rollup-check-failed']: (args: { reason: string }) => void;
|
|
66
70
|
['tx-count-check-failed']: (args: { minTxs: number; availableTxs: number }) => void;
|
|
@@ -71,7 +75,7 @@ export type SequencerEvents = {
|
|
|
71
75
|
sentActions?: Action[];
|
|
72
76
|
expiredActions?: Action[];
|
|
73
77
|
}) => void;
|
|
74
|
-
['block-published']: (args: { blockNumber:
|
|
78
|
+
['block-published']: (args: { blockNumber: BlockNumber; slot: number }) => void;
|
|
75
79
|
};
|
|
76
80
|
|
|
77
81
|
/**
|
|
@@ -98,6 +102,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
98
102
|
|
|
99
103
|
private governanceProposerPayload: EthAddress | undefined;
|
|
100
104
|
|
|
105
|
+
/** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
|
|
106
|
+
private lastSlotForVoteWhenSyncFailed: SlotNumber | undefined;
|
|
107
|
+
|
|
108
|
+
/** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */
|
|
109
|
+
private lastSlotForValidationBlock: SlotNumber | undefined;
|
|
110
|
+
|
|
101
111
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
|
|
102
112
|
protected timetable!: SequencerTimetable;
|
|
103
113
|
protected enforceTimeTable: boolean = false;
|
|
@@ -129,6 +139,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
129
139
|
) {
|
|
130
140
|
super();
|
|
131
141
|
|
|
142
|
+
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
143
|
+
if (this.config.fishermanMode) {
|
|
144
|
+
this.log = log.createChild('[FISHERMAN]');
|
|
145
|
+
}
|
|
146
|
+
|
|
132
147
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
133
148
|
// Initialize config
|
|
134
149
|
this.updateConfig(this.config);
|
|
@@ -207,6 +222,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
public async init() {
|
|
225
|
+
// Takes ~3s to precompute some tables.
|
|
226
|
+
getKzg();
|
|
210
227
|
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
211
228
|
}
|
|
212
229
|
|
|
@@ -214,7 +231,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
214
231
|
* Starts the sequencer and moves to IDLE state.
|
|
215
232
|
*/
|
|
216
233
|
public start() {
|
|
217
|
-
this.runningPromise = new RunningPromise(this.
|
|
234
|
+
this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
|
|
218
235
|
this.setState(SequencerState.IDLE, undefined, { force: true });
|
|
219
236
|
this.runningPromise.start();
|
|
220
237
|
this.log.info('Started sequencer');
|
|
@@ -248,27 +265,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
248
265
|
* - Submit block
|
|
249
266
|
* - If our block for some reason is not included, revert the state
|
|
250
267
|
*/
|
|
251
|
-
protected async
|
|
268
|
+
protected async work() {
|
|
252
269
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
270
|
+
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
253
271
|
|
|
254
|
-
// Check
|
|
255
|
-
|
|
272
|
+
// Check we have not already published a block for this slot (cheapest check)
|
|
273
|
+
if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
|
|
274
|
+
this.log.debug(
|
|
275
|
+
`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
|
|
276
|
+
);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
256
279
|
|
|
257
|
-
//
|
|
280
|
+
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
281
|
+
const syncedTo = await this.checkSync({ ts, slot });
|
|
258
282
|
if (!syncedTo) {
|
|
283
|
+
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
259
284
|
return;
|
|
260
285
|
}
|
|
261
286
|
|
|
262
287
|
const chainTipArchive = syncedTo.archive;
|
|
263
|
-
const newBlockNumber = syncedTo.blockNumber + 1;
|
|
264
|
-
|
|
265
|
-
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
288
|
+
const newBlockNumber = BlockNumber(syncedTo.blockNumber + 1);
|
|
266
289
|
|
|
267
|
-
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
268
|
-
|
|
269
|
-
// Check that the archiver and dependencies have synced to the previous L1 slot at least
|
|
270
|
-
// TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
|
|
271
|
-
// cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
|
|
272
290
|
const syncLogData = {
|
|
273
291
|
now,
|
|
274
292
|
syncedToL1Ts: syncedTo.l1Timestamp,
|
|
@@ -280,84 +298,70 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
280
298
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
281
299
|
};
|
|
282
300
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
syncLogData,
|
|
287
|
-
);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Check that the slot is not taken by a block already
|
|
292
|
-
if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
|
|
293
|
-
this.log.debug(
|
|
294
|
-
`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
295
|
-
{ ...syncLogData, block: syncedTo.block.header.toInspect() },
|
|
296
|
-
);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
301
|
+
// Check that we are a proposer for the next slot
|
|
302
|
+
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
303
|
+
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
299
304
|
|
|
300
|
-
//
|
|
301
|
-
if (
|
|
302
|
-
this.
|
|
303
|
-
`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
|
|
304
|
-
{ ...syncLogData, block: this.lastBlockPublished.header.toInspect() },
|
|
305
|
-
);
|
|
305
|
+
// If we are not a proposer check if we should invalidate a invalid block, and bail
|
|
306
|
+
if (!canPropose) {
|
|
307
|
+
await this.considerInvalidatingBlock(syncedTo, slot);
|
|
306
308
|
return;
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
} catch (e) {
|
|
314
|
-
if (e instanceof NoCommitteeError) {
|
|
315
|
-
this.log.warn(
|
|
316
|
-
`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`,
|
|
317
|
-
);
|
|
311
|
+
// In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
|
|
312
|
+
if (this.config.fishermanMode) {
|
|
313
|
+
if (this.lastSlotForValidationBlock === slot) {
|
|
314
|
+
this.log.trace(`Already validated block building for slot ${slot} (skipping)`, { slot });
|
|
318
315
|
return;
|
|
319
316
|
}
|
|
317
|
+
this.log.debug(
|
|
318
|
+
`Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`,
|
|
319
|
+
{ slot, proposer: proposer?.toString() },
|
|
320
|
+
);
|
|
321
|
+
// Mark this slot as being validated
|
|
322
|
+
this.lastSlotForValidationBlock = slot;
|
|
320
323
|
}
|
|
321
324
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
...syncLogData,
|
|
330
|
-
});
|
|
331
|
-
// If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
|
|
332
|
-
if (!syncedTo.pendingChainValidationStatus.valid) {
|
|
333
|
-
// We pass i undefined here to get any available publisher.
|
|
334
|
-
const { publisher } = await this.publisherFactory.create(undefined);
|
|
335
|
-
await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
|
|
336
|
-
}
|
|
325
|
+
// Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
326
|
+
if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
|
|
327
|
+
this.log.warn(
|
|
328
|
+
`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
329
|
+
{ ...syncLogData, block: syncedTo.block.header.toInspect() },
|
|
330
|
+
);
|
|
331
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
|
|
337
332
|
return;
|
|
338
333
|
}
|
|
339
334
|
|
|
340
|
-
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
341
|
-
// if all the previous checks are good, but we do it just in case.
|
|
342
|
-
const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
|
|
343
|
-
|
|
344
335
|
// We now need to get ourselves a publisher.
|
|
345
336
|
// The returned attestor will be the one we provided if we provided one.
|
|
346
337
|
// Otherwise it will be a valid attestor for the returned publisher.
|
|
347
|
-
|
|
348
|
-
|
|
338
|
+
// In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
|
|
339
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(
|
|
340
|
+
this.config.fishermanMode ? undefined : proposer,
|
|
341
|
+
);
|
|
349
342
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
350
|
-
|
|
351
343
|
this.publisher = publisher;
|
|
352
344
|
|
|
345
|
+
// In fisherman mode, set the actual proposer's address for simulations
|
|
346
|
+
if (this.config.fishermanMode) {
|
|
347
|
+
if (proposer) {
|
|
348
|
+
publisher.setProposerAddressForSimulation(proposer);
|
|
349
|
+
this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Get proposer credentials
|
|
353
354
|
const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
|
|
354
355
|
const feeRecipient = this.validatorClient!.getFeeRecipientForAttestor(attestorAddress);
|
|
355
356
|
|
|
356
357
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
357
358
|
const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
|
|
359
|
+
|
|
360
|
+
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
361
|
+
// if all the previous checks are good, but we do it just in case.
|
|
358
362
|
const canProposeCheck = await publisher.canProposeAtNextEthBlock(
|
|
359
363
|
chainTipArchive,
|
|
360
|
-
|
|
364
|
+
proposer ?? EthAddress.ZERO,
|
|
361
365
|
invalidateBlock,
|
|
362
366
|
);
|
|
363
367
|
|
|
@@ -367,6 +371,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
367
371
|
syncLogData,
|
|
368
372
|
);
|
|
369
373
|
this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed' });
|
|
374
|
+
this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
|
|
370
375
|
return;
|
|
371
376
|
} else if (canProposeCheck.slot !== slot) {
|
|
372
377
|
this.log.warn(
|
|
@@ -374,20 +379,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
374
379
|
{ ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
|
|
375
380
|
);
|
|
376
381
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch' });
|
|
382
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
|
|
377
383
|
return;
|
|
378
|
-
} else if (canProposeCheck.
|
|
384
|
+
} else if (canProposeCheck.checkpointNumber !== CheckpointNumber.fromBlockNumber(newBlockNumber)) {
|
|
379
385
|
this.log.warn(
|
|
380
|
-
`Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.
|
|
386
|
+
`Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.checkpointNumber}.`,
|
|
381
387
|
{ ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
|
|
382
388
|
);
|
|
383
389
|
this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch' });
|
|
390
|
+
this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
|
|
384
391
|
return;
|
|
385
392
|
}
|
|
386
393
|
|
|
387
|
-
this.log.debug(
|
|
388
|
-
`Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
|
|
389
|
-
{ ...syncLogData, validatorAddresses },
|
|
390
|
-
);
|
|
394
|
+
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, { ...syncLogData });
|
|
391
395
|
|
|
392
396
|
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(
|
|
393
397
|
newBlockNumber,
|
|
@@ -396,53 +400,98 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
396
400
|
slot,
|
|
397
401
|
);
|
|
398
402
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
.catch(err => {
|
|
408
|
-
this.log.error(`Error enqueuing governance vote`, err, { blockNumber: newBlockNumber, slot });
|
|
409
|
-
return false;
|
|
410
|
-
})
|
|
411
|
-
: Promise.resolve(false);
|
|
412
|
-
|
|
413
|
-
const enqueueSlashingActionsPromise = this.slasherClient
|
|
414
|
-
? this.slasherClient
|
|
415
|
-
.getProposerActions(slot)
|
|
416
|
-
.then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
|
|
417
|
-
.catch(err => {
|
|
418
|
-
this.log.error(`Error enqueuing slashing actions`, err, { blockNumber: newBlockNumber, slot });
|
|
419
|
-
return false;
|
|
420
|
-
})
|
|
421
|
-
: Promise.resolve(false);
|
|
403
|
+
// Enqueue governance and slashing votes (returns promises that will be awaited later)
|
|
404
|
+
// In fisherman mode, we simulate slashing but don't actually publish to L1
|
|
405
|
+
const votesPromises = this.enqueueGovernanceAndSlashingVotes(
|
|
406
|
+
publisher,
|
|
407
|
+
attestorAddress,
|
|
408
|
+
slot,
|
|
409
|
+
newGlobalVariables.timestamp,
|
|
410
|
+
);
|
|
422
411
|
|
|
412
|
+
// Enqueues block invalidation
|
|
423
413
|
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
424
414
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
425
415
|
}
|
|
426
416
|
|
|
417
|
+
// Actual block building
|
|
427
418
|
this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
|
|
419
|
+
this.metrics.incOpenSlot(slot, proposer?.toString() ?? 'unknown');
|
|
420
|
+
const block: L2Block | undefined = await this.tryBuildBlockAndEnqueuePublish(
|
|
421
|
+
slot,
|
|
422
|
+
proposer,
|
|
423
|
+
newBlockNumber,
|
|
424
|
+
publisher,
|
|
425
|
+
newGlobalVariables,
|
|
426
|
+
chainTipArchive,
|
|
427
|
+
invalidateBlock,
|
|
428
|
+
);
|
|
428
429
|
|
|
429
|
-
|
|
430
|
+
// Wait until the voting promises have resolved, so all requests are enqueued
|
|
431
|
+
await Promise.all(votesPromises);
|
|
432
|
+
|
|
433
|
+
// In fisherman mode, we don't publish to L1
|
|
434
|
+
if (this.config.fishermanMode) {
|
|
435
|
+
// Clear pending requests
|
|
436
|
+
publisher.clearPendingRequests();
|
|
437
|
+
|
|
438
|
+
if (block) {
|
|
439
|
+
this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
|
|
440
|
+
blockNumber: newBlockNumber,
|
|
441
|
+
slot: Number(slot),
|
|
442
|
+
archive: block.archive.toString(),
|
|
443
|
+
txCount: block.body.txEffects.length,
|
|
444
|
+
});
|
|
445
|
+
this.lastBlockPublished = block;
|
|
446
|
+
this.metrics.recordBlockProposalSuccess();
|
|
447
|
+
} else {
|
|
448
|
+
// Block building failed in fisherman mode
|
|
449
|
+
this.log.warn(`Validation block building FAILED for slot ${slot}`, {
|
|
450
|
+
blockNumber: newBlockNumber,
|
|
451
|
+
slot: Number(slot),
|
|
452
|
+
});
|
|
453
|
+
this.metrics.recordBlockProposalFailed('block_build_failed');
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
// Normal mode: send the tx to L1
|
|
457
|
+
const l1Response = await publisher.sendRequests();
|
|
458
|
+
const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
|
|
459
|
+
if (proposedBlock) {
|
|
460
|
+
this.lastBlockPublished = block;
|
|
461
|
+
this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
|
|
462
|
+
await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
|
|
463
|
+
} else if (block) {
|
|
464
|
+
this.emit('block-publish-failed', l1Response ?? {});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
this.setState(SequencerState.IDLE, undefined);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Tries building a block proposal, and if successful, enqueues it for publishing. */
|
|
472
|
+
private async tryBuildBlockAndEnqueuePublish(
|
|
473
|
+
slot: SlotNumber,
|
|
474
|
+
proposer: EthAddress | undefined,
|
|
475
|
+
newBlockNumber: BlockNumber,
|
|
476
|
+
publisher: SequencerPublisher,
|
|
477
|
+
newGlobalVariables: GlobalVariables,
|
|
478
|
+
chainTipArchive: Fr,
|
|
479
|
+
invalidateBlock: InvalidateBlockRequest | undefined,
|
|
480
|
+
) {
|
|
430
481
|
this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
|
|
431
|
-
proposer
|
|
432
|
-
coinbase,
|
|
482
|
+
proposer,
|
|
433
483
|
publisher: publisher.getSenderAddress(),
|
|
434
|
-
feeRecipient,
|
|
435
484
|
globalVariables: newGlobalVariables.toInspect(),
|
|
436
485
|
chainTipArchive,
|
|
437
486
|
blockNumber: newBlockNumber,
|
|
438
487
|
slot,
|
|
439
488
|
});
|
|
440
489
|
|
|
441
|
-
// If I created a "partial" header here that should make our job much easier.
|
|
442
490
|
const proposalHeader = CheckpointHeader.from({
|
|
443
491
|
...newGlobalVariables,
|
|
444
492
|
timestamp: newGlobalVariables.timestamp,
|
|
445
493
|
lastArchiveRoot: chainTipArchive,
|
|
494
|
+
blockHeadersHash: Fr.ZERO,
|
|
446
495
|
contentCommitment: ContentCommitment.empty(),
|
|
447
496
|
totalManaUsed: Fr.ZERO,
|
|
448
497
|
});
|
|
@@ -459,7 +508,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
459
508
|
pendingTxs,
|
|
460
509
|
proposalHeader,
|
|
461
510
|
newGlobalVariables,
|
|
462
|
-
|
|
511
|
+
proposer,
|
|
463
512
|
invalidateBlock,
|
|
464
513
|
publisher,
|
|
465
514
|
);
|
|
@@ -470,6 +519,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
470
519
|
} else {
|
|
471
520
|
this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
|
|
472
521
|
}
|
|
522
|
+
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
473
523
|
}
|
|
474
524
|
} else {
|
|
475
525
|
this.log.verbose(
|
|
@@ -477,27 +527,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
477
527
|
{ chainTipArchive, blockNumber: newBlockNumber, slot },
|
|
478
528
|
);
|
|
479
529
|
this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
|
|
530
|
+
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
480
531
|
}
|
|
481
|
-
|
|
482
|
-
await Promise.all([enqueueGovernanceSignalPromise, enqueueSlashingActionsPromise]);
|
|
483
|
-
|
|
484
|
-
const l1Response = await publisher.sendRequests();
|
|
485
|
-
const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
|
|
486
|
-
if (proposedBlock) {
|
|
487
|
-
this.lastBlockPublished = block;
|
|
488
|
-
this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
|
|
489
|
-
await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
|
|
490
|
-
} else if (block) {
|
|
491
|
-
this.emit('block-publish-failed', l1Response ?? {});
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
this.setState(SequencerState.IDLE, undefined);
|
|
532
|
+
return block;
|
|
495
533
|
}
|
|
496
534
|
|
|
497
535
|
@trackSpan('Sequencer.work')
|
|
498
|
-
protected async
|
|
536
|
+
protected async safeWork() {
|
|
499
537
|
try {
|
|
500
|
-
await this.
|
|
538
|
+
await this.work();
|
|
501
539
|
} catch (err) {
|
|
502
540
|
if (err instanceof SequencerTooSlowError) {
|
|
503
541
|
// Log as warn only if we had to abort halfway through the block proposal
|
|
@@ -520,13 +558,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
520
558
|
* @param slotNumber - The current slot number.
|
|
521
559
|
* @param force - Whether to force the transition even if the sequencer is stopped.
|
|
522
560
|
*/
|
|
523
|
-
setState(proposedState: SequencerStateWithSlot, slotNumber:
|
|
561
|
+
setState(proposedState: SequencerStateWithSlot, slotNumber: SlotNumber, opts?: { force?: boolean }): void;
|
|
524
562
|
setState(
|
|
525
563
|
proposedState: Exclude<SequencerState, SequencerStateWithSlot>,
|
|
526
564
|
slotNumber?: undefined,
|
|
527
565
|
opts?: { force?: boolean },
|
|
528
566
|
): void;
|
|
529
|
-
setState(proposedState: SequencerState, slotNumber:
|
|
567
|
+
setState(proposedState: SequencerState, slotNumber: SlotNumber | undefined, opts: { force?: boolean } = {}): void {
|
|
530
568
|
if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
|
|
531
569
|
this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
|
|
532
570
|
throw new SequencerInterruptedError();
|
|
@@ -567,7 +605,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
567
605
|
await this.p2pClient.deleteTxs(failedTxHashes);
|
|
568
606
|
}
|
|
569
607
|
|
|
570
|
-
protected getBlockBuilderOptions(slot:
|
|
608
|
+
protected getBlockBuilderOptions(slot: SlotNumber): PublicProcessorLimits {
|
|
571
609
|
// Deadline for processing depends on whether we're proposing a block
|
|
572
610
|
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
573
611
|
const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
|
|
@@ -580,7 +618,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
580
618
|
maxTransactions: this.maxTxsPerBlock,
|
|
581
619
|
maxBlockSize: this.maxBlockSizeInBytes,
|
|
582
620
|
maxBlockGas: this.maxBlockGas,
|
|
583
|
-
maxBlobFields:
|
|
621
|
+
maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
|
|
584
622
|
deadline,
|
|
585
623
|
};
|
|
586
624
|
}
|
|
@@ -610,14 +648,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
610
648
|
await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
|
|
611
649
|
|
|
612
650
|
const blockNumber = newGlobalVariables.blockNumber;
|
|
613
|
-
const
|
|
614
|
-
const
|
|
651
|
+
const checkpointNumber = CheckpointNumber.fromBlockNumber(blockNumber);
|
|
652
|
+
const slot = proposalHeader.slotNumber;
|
|
653
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
615
654
|
|
|
616
655
|
const workTimer = new Timer();
|
|
617
656
|
this.setState(SequencerState.CREATING_BLOCK, slot);
|
|
618
657
|
|
|
619
658
|
try {
|
|
620
|
-
const blockBuilderOptions = this.getBlockBuilderOptions(
|
|
659
|
+
const blockBuilderOptions = this.getBlockBuilderOptions(slot);
|
|
621
660
|
const buildBlockRes = await this.blockBuilder.buildBlock(
|
|
622
661
|
pendingTxs,
|
|
623
662
|
l1ToL2Messages,
|
|
@@ -665,19 +704,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
665
704
|
},
|
|
666
705
|
);
|
|
667
706
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
if (
|
|
671
|
-
this.log.
|
|
707
|
+
// In fisherman mode, skip attestation collection
|
|
708
|
+
let attestationsAndSigners: CommitteeAttestationsAndSigners;
|
|
709
|
+
if (this.config.fishermanMode) {
|
|
710
|
+
this.log.debug('Skipping attestation collection');
|
|
711
|
+
attestationsAndSigners = CommitteeAttestationsAndSigners.empty();
|
|
712
|
+
} else {
|
|
713
|
+
this.log.debug('Collecting attestations');
|
|
714
|
+
attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
|
|
715
|
+
this.log.verbose(
|
|
716
|
+
`Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`,
|
|
717
|
+
{ blockHash, blockNumber, slot },
|
|
718
|
+
);
|
|
672
719
|
}
|
|
673
720
|
|
|
674
|
-
|
|
675
|
-
const attestationsAndSignersSignature =
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
721
|
+
// In fisherman mode, skip attestation signing
|
|
722
|
+
const attestationsAndSignersSignature =
|
|
723
|
+
this.config.fishermanMode || !this.validatorClient
|
|
724
|
+
? Signature.empty()
|
|
725
|
+
: await this.validatorClient.signAttestationsAndSigners(
|
|
726
|
+
attestationsAndSigners,
|
|
727
|
+
proposerAddress ?? publisher.getSenderAddress(),
|
|
728
|
+
);
|
|
681
729
|
|
|
682
730
|
await this.enqueuePublishL2Block(
|
|
683
731
|
block,
|
|
@@ -703,8 +751,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
703
751
|
block: L2Block,
|
|
704
752
|
txs: Tx[],
|
|
705
753
|
proposerAddress: EthAddress | undefined,
|
|
706
|
-
): Promise<
|
|
707
|
-
const { committee } = await this.epochCache.getCommittee(block.
|
|
754
|
+
): Promise<CommitteeAttestationsAndSigners> {
|
|
755
|
+
const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
|
|
708
756
|
|
|
709
757
|
// We checked above that the committee is defined, so this should never happen.
|
|
710
758
|
if (!committee) {
|
|
@@ -713,20 +761,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
713
761
|
|
|
714
762
|
if (committee.length === 0) {
|
|
715
763
|
this.log.verbose(`Attesting committee is empty`);
|
|
716
|
-
return
|
|
764
|
+
return CommitteeAttestationsAndSigners.empty();
|
|
717
765
|
} else {
|
|
718
766
|
this.log.debug(`Attesting committee length is ${committee.length}`);
|
|
719
767
|
}
|
|
720
768
|
|
|
721
769
|
if (!this.validatorClient) {
|
|
722
|
-
|
|
723
|
-
this.log.error(msg);
|
|
724
|
-
throw new Error(msg);
|
|
770
|
+
throw new Error('Missing validator client: Cannot collect attestations');
|
|
725
771
|
}
|
|
726
772
|
|
|
727
773
|
const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
|
|
728
774
|
|
|
729
|
-
const slotNumber = block.header.globalVariables.slotNumber
|
|
775
|
+
const slotNumber = block.header.globalVariables.slotNumber;
|
|
730
776
|
this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
|
|
731
777
|
|
|
732
778
|
this.log.debug('Creating block proposal for validators');
|
|
@@ -738,7 +784,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
738
784
|
block.header.globalVariables.blockNumber,
|
|
739
785
|
block.getCheckpointHeader(),
|
|
740
786
|
block.archive.root,
|
|
741
|
-
block.header.state,
|
|
742
787
|
txs,
|
|
743
788
|
proposerAddress,
|
|
744
789
|
blockProposalOptions,
|
|
@@ -751,7 +796,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
751
796
|
if (this.config.skipCollectingAttestations) {
|
|
752
797
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
753
798
|
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
754
|
-
return orderAttestations(attestations ?? [], committee);
|
|
799
|
+
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
755
800
|
}
|
|
756
801
|
|
|
757
802
|
this.log.debug('Broadcasting block proposal to validators');
|
|
@@ -777,13 +822,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
777
822
|
|
|
778
823
|
// note: the smart contract requires that the signatures are provided in the order of the committee
|
|
779
824
|
const sorted = orderAttestations(attestations, committee);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
this.
|
|
784
|
-
unfreeze(nonEmpty[randomIndex]).signature = Signature.random();
|
|
825
|
+
|
|
826
|
+
// manipulate the attestations if we've been configured to do so
|
|
827
|
+
if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
|
|
828
|
+
return this.manipulateAttestations(block, epoch, seed, committee, sorted);
|
|
785
829
|
}
|
|
786
|
-
|
|
830
|
+
|
|
831
|
+
return new CommitteeAttestationsAndSigners(sorted);
|
|
787
832
|
} catch (err) {
|
|
788
833
|
if (err && err instanceof AttestationTimeoutError) {
|
|
789
834
|
collectedAttestationsCount = err.collectedCount;
|
|
@@ -794,6 +839,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
794
839
|
}
|
|
795
840
|
}
|
|
796
841
|
|
|
842
|
+
/** Breaks the attestations before publishing based on attack configs */
|
|
843
|
+
private manipulateAttestations(
|
|
844
|
+
block: L2Block,
|
|
845
|
+
epoch: EpochNumber,
|
|
846
|
+
seed: bigint,
|
|
847
|
+
committee: EthAddress[],
|
|
848
|
+
attestations: CommitteeAttestation[],
|
|
849
|
+
) {
|
|
850
|
+
// Compute the proposer index in the committee, since we dont want to tweak it.
|
|
851
|
+
// Otherwise, the L1 rollup contract will reject the block outright.
|
|
852
|
+
const proposerIndex = Number(
|
|
853
|
+
this.epochCache.computeProposerIndex(block.slot, epoch, seed, BigInt(committee.length)),
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
if (this.config.injectFakeAttestation) {
|
|
857
|
+
// Find non-empty attestations that are not from the proposer
|
|
858
|
+
const nonProposerIndices: number[] = [];
|
|
859
|
+
for (let i = 0; i < attestations.length; i++) {
|
|
860
|
+
if (!attestations[i].signature.isEmpty() && i !== proposerIndex) {
|
|
861
|
+
nonProposerIndices.push(i);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (nonProposerIndices.length > 0) {
|
|
865
|
+
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
866
|
+
this.log.warn(`Injecting fake attestation in block ${block.number} at index ${targetIndex}`);
|
|
867
|
+
unfreeze(attestations[targetIndex]).signature = Signature.random();
|
|
868
|
+
}
|
|
869
|
+
return new CommitteeAttestationsAndSigners(attestations);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (this.config.shuffleAttestationOrdering) {
|
|
873
|
+
this.log.warn(`Shuffling attestation ordering in block ${block.number} (proposer index ${proposerIndex})`);
|
|
874
|
+
|
|
875
|
+
const shuffled = [...attestations];
|
|
876
|
+
const [i, j] = [(proposerIndex + 1) % shuffled.length, (proposerIndex + 2) % shuffled.length];
|
|
877
|
+
const valueI = shuffled[i];
|
|
878
|
+
const valueJ = shuffled[j];
|
|
879
|
+
shuffled[i] = valueJ;
|
|
880
|
+
shuffled[j] = valueI;
|
|
881
|
+
|
|
882
|
+
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
883
|
+
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return new CommitteeAttestationsAndSigners(attestations);
|
|
887
|
+
}
|
|
888
|
+
|
|
797
889
|
/**
|
|
798
890
|
* Publishes the L2Block to the rollup contract.
|
|
799
891
|
* @param block - The L2Block to be published.
|
|
@@ -809,10 +901,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
809
901
|
publisher: SequencerPublisher,
|
|
810
902
|
): Promise<void> {
|
|
811
903
|
// Publishes new block to the network and awaits the tx to be mined
|
|
812
|
-
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber
|
|
904
|
+
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
|
|
813
905
|
|
|
814
906
|
// Time out tx at the end of the slot
|
|
815
|
-
const slot = block.header.globalVariables.slotNumber
|
|
907
|
+
const slot = block.header.globalVariables.slotNumber;
|
|
816
908
|
const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
|
|
817
909
|
|
|
818
910
|
const enqueued = await publisher.enqueueProposeL2Block(
|
|
@@ -833,18 +925,31 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
833
925
|
/**
|
|
834
926
|
* Returns whether all dependencies have caught up.
|
|
835
927
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
836
|
-
* @returns Boolean indicating if our dependencies are synced to the latest block.
|
|
837
928
|
*/
|
|
838
|
-
protected async
|
|
929
|
+
protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<
|
|
839
930
|
| {
|
|
840
931
|
block?: L2Block;
|
|
841
|
-
blockNumber:
|
|
932
|
+
blockNumber: BlockNumber;
|
|
842
933
|
archive: Fr;
|
|
843
934
|
l1Timestamp: bigint;
|
|
844
935
|
pendingChainValidationStatus: ValidateBlockResult;
|
|
845
936
|
}
|
|
846
937
|
| undefined
|
|
847
938
|
> {
|
|
939
|
+
// Check that the archiver and dependencies have synced to the previous L1 slot at least
|
|
940
|
+
// TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
|
|
941
|
+
// cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
|
|
942
|
+
const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
|
|
943
|
+
const { slot, ts } = args;
|
|
944
|
+
if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
|
|
945
|
+
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
946
|
+
slot,
|
|
947
|
+
ts,
|
|
948
|
+
l1Timestamp,
|
|
949
|
+
});
|
|
950
|
+
return undefined;
|
|
951
|
+
}
|
|
952
|
+
|
|
848
953
|
const syncedBlocks = await Promise.all([
|
|
849
954
|
this.worldState.status().then(({ syncSummary }) => ({
|
|
850
955
|
number: syncSummary.latestBlockNumber,
|
|
@@ -853,49 +958,203 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
853
958
|
this.l2BlockSource.getL2Tips().then(t => t.latest),
|
|
854
959
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
855
960
|
this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
|
|
856
|
-
this.l2BlockSource.getL1Timestamp(),
|
|
857
961
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
858
962
|
] as const);
|
|
859
963
|
|
|
860
|
-
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource,
|
|
861
|
-
syncedBlocks;
|
|
964
|
+
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
|
|
862
965
|
|
|
863
|
-
//
|
|
864
|
-
//
|
|
966
|
+
// Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
|
|
967
|
+
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
865
968
|
const result =
|
|
866
|
-
l2BlockSource.
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
l1ToL2MessageSource.hash === l2BlockSource.hash;
|
|
871
|
-
|
|
872
|
-
const logData = { worldState, l2BlockSource, p2p, l1ToL2MessageSource };
|
|
873
|
-
this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
|
|
969
|
+
(l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
|
|
970
|
+
(worldState.hash === l2BlockSource.hash &&
|
|
971
|
+
p2p.hash === l2BlockSource.hash &&
|
|
972
|
+
l1ToL2MessageSource.hash === l2BlockSource.hash);
|
|
874
973
|
|
|
875
974
|
if (!result) {
|
|
975
|
+
this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
|
|
876
976
|
return undefined;
|
|
877
977
|
}
|
|
878
978
|
|
|
979
|
+
// Special case for genesis state
|
|
879
980
|
const blockNumber = worldState.number;
|
|
880
|
-
if (blockNumber
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
981
|
+
if (blockNumber < INITIAL_L2_BLOCK_NUM) {
|
|
982
|
+
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
983
|
+
return { blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1), archive, l1Timestamp, pendingChainValidationStatus };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const block = await this.l2BlockSource.getBlock(blockNumber);
|
|
987
|
+
if (!block) {
|
|
988
|
+
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
989
|
+
this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
|
|
990
|
+
return undefined;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return {
|
|
994
|
+
block,
|
|
995
|
+
blockNumber: block.number,
|
|
996
|
+
archive: block.archive.root,
|
|
997
|
+
l1Timestamp,
|
|
998
|
+
pendingChainValidationStatus,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Enqueues governance and slashing votes with the publisher. Does not block.
|
|
1004
|
+
* @param publisher - The publisher to enqueue votes with
|
|
1005
|
+
* @param attestorAddress - The attestor address to use for signing
|
|
1006
|
+
* @param slot - The slot number
|
|
1007
|
+
* @param timestamp - The timestamp for the votes
|
|
1008
|
+
* @param context - Optional context for logging (e.g., block number)
|
|
1009
|
+
* @returns A tuple of [governanceEnqueued, slashingEnqueued]
|
|
1010
|
+
*/
|
|
1011
|
+
protected enqueueGovernanceAndSlashingVotes(
|
|
1012
|
+
publisher: SequencerPublisher,
|
|
1013
|
+
attestorAddress: EthAddress,
|
|
1014
|
+
slot: SlotNumber,
|
|
1015
|
+
timestamp: bigint,
|
|
1016
|
+
): [Promise<boolean> | undefined, Promise<boolean> | undefined] {
|
|
1017
|
+
try {
|
|
1018
|
+
const signerFn = (msg: TypedDataDefinition) =>
|
|
1019
|
+
this.validatorClient!.signWithAddress(attestorAddress, msg).then(s => s.toString());
|
|
1020
|
+
|
|
1021
|
+
const enqueueGovernancePromise =
|
|
1022
|
+
this.governanceProposerPayload && !this.governanceProposerPayload.isZero()
|
|
1023
|
+
? publisher
|
|
1024
|
+
.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn)
|
|
1025
|
+
.catch(err => {
|
|
1026
|
+
this.log.error(`Error enqueuing governance vote`, err, { slot });
|
|
1027
|
+
return false;
|
|
1028
|
+
})
|
|
1029
|
+
: undefined;
|
|
1030
|
+
|
|
1031
|
+
const enqueueSlashingPromise = this.slasherClient
|
|
1032
|
+
? this.slasherClient
|
|
1033
|
+
.getProposerActions(slot)
|
|
1034
|
+
.then(actions => {
|
|
1035
|
+
// Record metrics for fisherman mode
|
|
1036
|
+
if (this.config.fishermanMode && actions.length > 0) {
|
|
1037
|
+
this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
|
|
1038
|
+
slot,
|
|
1039
|
+
actionCount: actions.length,
|
|
1040
|
+
});
|
|
1041
|
+
this.metrics.recordSlashingAttempt(actions.length);
|
|
1042
|
+
}
|
|
1043
|
+
// Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
|
|
1044
|
+
return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
|
|
1045
|
+
})
|
|
1046
|
+
.catch(err => {
|
|
1047
|
+
this.log.error(`Error enqueuing slashing actions`, err, { slot });
|
|
1048
|
+
return false;
|
|
1049
|
+
})
|
|
1050
|
+
: undefined;
|
|
1051
|
+
|
|
1052
|
+
return [enqueueGovernancePromise, enqueueSlashingPromise];
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
this.log.error(`Error enqueueing governance and slashing votes`, err);
|
|
1055
|
+
return [undefined, undefined];
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Checks if we are the proposer for the next slot.
|
|
1061
|
+
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
1062
|
+
*/
|
|
1063
|
+
protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
1064
|
+
let proposer: EthAddress | undefined;
|
|
1065
|
+
|
|
1066
|
+
try {
|
|
1067
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
if (e instanceof NoCommitteeError) {
|
|
1070
|
+
this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
|
|
1071
|
+
return [false, undefined];
|
|
886
1072
|
}
|
|
1073
|
+
this.log.error(`Error getting proposer for slot ${slot}`, e);
|
|
1074
|
+
return [false, undefined];
|
|
1075
|
+
}
|
|
887
1076
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
|
|
1077
|
+
// If proposer is undefined, then the committee is empty and anyone may propose
|
|
1078
|
+
if (proposer === undefined) {
|
|
1079
|
+
return [true, undefined];
|
|
1080
|
+
}
|
|
1081
|
+
// In fisherman mode, just return the current proposer
|
|
1082
|
+
if (this.config.fishermanMode) {
|
|
1083
|
+
return [true, proposer];
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const validatorAddresses = this.validatorClient!.getValidatorAddresses();
|
|
1087
|
+
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
1088
|
+
|
|
1089
|
+
if (!weAreProposer) {
|
|
1090
|
+
this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
|
|
1091
|
+
return [false, proposer];
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return [true, proposer];
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
|
|
1099
|
+
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
1100
|
+
*/
|
|
1101
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
1102
|
+
const { slot, ts } = args;
|
|
1103
|
+
|
|
1104
|
+
// Prevent duplicate attempts in the same slot
|
|
1105
|
+
if (this.lastSlotForVoteWhenSyncFailed === slot) {
|
|
1106
|
+
this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Check if we're past the max time for initializing a proposal
|
|
1111
|
+
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
1112
|
+
const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
|
|
1113
|
+
|
|
1114
|
+
// If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
|
|
1115
|
+
// We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
|
|
1116
|
+
if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
|
|
1117
|
+
this.log.trace(`Not attempting to vote since there is still for block building`, {
|
|
1118
|
+
secondsIntoSlot,
|
|
1119
|
+
maxAllowedTime,
|
|
1120
|
+
});
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
|
|
1125
|
+
secondsIntoSlot,
|
|
1126
|
+
maxAllowedTime,
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// Check if we're a proposer or proposal is open
|
|
1130
|
+
const [canPropose, proposer] = await this.checkCanPropose(slot);
|
|
1131
|
+
if (!canPropose) {
|
|
1132
|
+
this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// Mark this slot as attempted
|
|
1137
|
+
this.lastSlotForVoteWhenSyncFailed = slot;
|
|
1138
|
+
|
|
1139
|
+
// Get a publisher for voting
|
|
1140
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
1141
|
+
|
|
1142
|
+
this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
|
|
1143
|
+
attestorAddress,
|
|
1144
|
+
slot,
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// Enqueue governance and slashing votes using the shared helper method
|
|
1148
|
+
const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
|
|
1149
|
+
await Promise.all(votesPromises);
|
|
1150
|
+
|
|
1151
|
+
if (votesPromises.every(p => !p)) {
|
|
1152
|
+
this.log.debug(`No votes to enqueue for slot ${slot}`);
|
|
1153
|
+
return;
|
|
898
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
|
|
1157
|
+
await publisher.sendRequests();
|
|
899
1158
|
}
|
|
900
1159
|
|
|
901
1160
|
/**
|
|
@@ -905,10 +1164,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
905
1164
|
* and if they fail, any sequencer will try as well.
|
|
906
1165
|
*/
|
|
907
1166
|
protected async considerInvalidatingBlock(
|
|
908
|
-
syncedTo: NonNullable<Awaited<ReturnType<Sequencer['
|
|
909
|
-
currentSlot:
|
|
910
|
-
ourValidatorAddresses: EthAddress[],
|
|
911
|
-
publisher: SequencerPublisher,
|
|
1167
|
+
syncedTo: NonNullable<Awaited<ReturnType<Sequencer['checkSync']>>>,
|
|
1168
|
+
currentSlot: SlotNumber,
|
|
912
1169
|
): Promise<void> {
|
|
913
1170
|
const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
|
|
914
1171
|
if (pendingChainValidationStatus.valid) {
|
|
@@ -918,6 +1175,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
918
1175
|
const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
|
|
919
1176
|
const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
|
|
920
1177
|
const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
|
|
1178
|
+
const ourValidatorAddresses = this.validatorClient!.getValidatorAddresses();
|
|
921
1179
|
|
|
922
1180
|
const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
|
|
923
1181
|
this.config;
|
|
@@ -953,6 +1211,24 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
953
1211
|
return;
|
|
954
1212
|
}
|
|
955
1213
|
|
|
1214
|
+
let validatorToUse: EthAddress;
|
|
1215
|
+
if (invalidateAsCommitteeMember) {
|
|
1216
|
+
// When invalidating as a committee member, use first validator that's actually in the committee
|
|
1217
|
+
const { committee } = await this.epochCache.getCommittee(currentSlot);
|
|
1218
|
+
if (committee) {
|
|
1219
|
+
const committeeSet = new Set(committee.map(addr => addr.toString()));
|
|
1220
|
+
validatorToUse =
|
|
1221
|
+
ourValidatorAddresses.find(addr => committeeSet.has(addr.toString())) ?? ourValidatorAddresses[0];
|
|
1222
|
+
} else {
|
|
1223
|
+
validatorToUse = ourValidatorAddresses[0];
|
|
1224
|
+
}
|
|
1225
|
+
} else {
|
|
1226
|
+
// When invalidating as a non-committee member, use the first validator
|
|
1227
|
+
validatorToUse = ourValidatorAddresses[0];
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const { publisher } = await this.publisherFactory.create(validatorToUse);
|
|
1231
|
+
|
|
956
1232
|
const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
|
|
957
1233
|
if (!invalidateBlock) {
|
|
958
1234
|
this.log.warn(`Failed to simulate invalidate block`, logData);
|
|
@@ -967,14 +1243,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
967
1243
|
);
|
|
968
1244
|
|
|
969
1245
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
970
|
-
|
|
1246
|
+
|
|
1247
|
+
if (!this.config.fishermanMode) {
|
|
1248
|
+
await publisher.sendRequests();
|
|
1249
|
+
} else {
|
|
1250
|
+
this.log.info('Invalidating block in fisherman mode, clearing pending requests');
|
|
1251
|
+
publisher.clearPendingRequests();
|
|
1252
|
+
}
|
|
971
1253
|
}
|
|
972
1254
|
|
|
973
|
-
private getSlotStartBuildTimestamp(slotNumber:
|
|
1255
|
+
private getSlotStartBuildTimestamp(slotNumber: SlotNumber): number {
|
|
974
1256
|
return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
|
|
975
1257
|
}
|
|
976
1258
|
|
|
977
|
-
private getSecondsIntoSlot(slotNumber:
|
|
1259
|
+
private getSecondsIntoSlot(slotNumber: SlotNumber): number {
|
|
978
1260
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
|
|
979
1261
|
return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
|
|
980
1262
|
}
|