@aztec/sequencer-client 3.0.0-canary.a9708bd → 3.0.0-devnet.20251212
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 +7 -6
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +23 -14
- package/dest/config.d.ts +5 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +21 -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 +9 -10
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +19 -17
- package/dest/publisher/index.d.ts +2 -2
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +9 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +8 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts +2 -2
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +1 -1
- package/dest/publisher/sequencer-publisher.d.ts +53 -50
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +187 -132
- package/dest/sequencer/block_builder.d.ts +6 -8
- package/dest/sequencer/block_builder.d.ts.map +1 -1
- package/dest/sequencer/block_builder.js +18 -9
- package/dest/sequencer/config.d.ts +2 -2
- package/dest/sequencer/config.d.ts.map +1 -1
- package/dest/sequencer/errors.d.ts +11 -0
- package/dest/sequencer/errors.d.ts.map +1 -0
- package/dest/sequencer/errors.js +15 -0
- package/dest/sequencer/index.d.ts +1 -1
- package/dest/sequencer/metrics.d.ts +17 -20
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +59 -87
- package/dest/sequencer/sequencer.d.ts +54 -32
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +458 -184
- package/dest/sequencer/timetable.d.ts +4 -8
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +3 -10
- package/dest/sequencer/utils.d.ts +11 -25
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +9 -24
- 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 +12 -9
- package/package.json +32 -31
- package/src/client/sequencer-client.ts +24 -18
- package/src/config.ts +23 -6
- package/src/global_variable_builder/global_builder.ts +19 -17
- package/src/publisher/config.ts +29 -27
- package/src/publisher/index.ts +1 -1
- package/src/publisher/sequencer-publisher-factory.ts +16 -3
- package/src/publisher/sequencer-publisher-metrics.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +257 -183
- package/src/sequencer/block_builder.ts +23 -28
- package/src/sequencer/config.ts +1 -1
- package/src/sequencer/errors.ts +21 -0
- package/src/sequencer/metrics.ts +71 -98
- package/src/sequencer/sequencer.ts +546 -237
- package/src/sequencer/timetable.ts +10 -14
- package/src/sequencer/utils.ts +10 -24
- package/src/test/index.ts +1 -1
- package/src/tx_validator/tx_validator_factory.ts +13 -7
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { L2Block } from '@aztec/aztec.js/block';
|
|
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';
|
|
9
|
+
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
11
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
|
-
import {
|
|
12
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
13
|
import { createLogger } from '@aztec/foundation/log';
|
|
9
14
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
10
15
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
11
|
-
import type
|
|
16
|
+
import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
|
|
12
17
|
import type { P2P } from '@aztec/p2p';
|
|
13
18
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
14
|
-
import
|
|
15
|
-
|
|
19
|
+
import {
|
|
20
|
+
CommitteeAttestation,
|
|
21
|
+
CommitteeAttestationsAndSigners,
|
|
22
|
+
type L2BlockSource,
|
|
23
|
+
MaliciousCommitteeAttestationsAndSigners,
|
|
24
|
+
type ValidateBlockResult,
|
|
25
|
+
} from '@aztec/stdlib/block';
|
|
26
|
+
import { type L1RollupConstants, getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
16
27
|
import { Gas } from '@aztec/stdlib/gas';
|
|
17
28
|
import {
|
|
18
29
|
type IFullNodeBlockBuilder,
|
|
@@ -23,17 +34,11 @@ import {
|
|
|
23
34
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
24
35
|
import type { BlockProposalOptions } from '@aztec/stdlib/p2p';
|
|
25
36
|
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
37
|
+
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
26
38
|
import { pickFromSchema } from '@aztec/stdlib/schemas';
|
|
27
39
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
28
40
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
29
|
-
import {
|
|
30
|
-
ContentCommitment,
|
|
31
|
-
type FailedTx,
|
|
32
|
-
GlobalVariables,
|
|
33
|
-
ProposedBlockHeader,
|
|
34
|
-
Tx,
|
|
35
|
-
type TxHash,
|
|
36
|
-
} from '@aztec/stdlib/tx';
|
|
41
|
+
import { ContentCommitment, type FailedTx, GlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
37
42
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
38
43
|
import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
39
44
|
import type { ValidatorClient } from '@aztec/validator-client';
|
|
@@ -45,8 +50,9 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
|
|
|
45
50
|
import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
|
|
46
51
|
import type { Action, InvalidateBlockRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
47
52
|
import type { SequencerConfig } from './config.js';
|
|
53
|
+
import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
|
|
48
54
|
import { SequencerMetrics } from './metrics.js';
|
|
49
|
-
import { SequencerTimetable
|
|
55
|
+
import { SequencerTimetable } from './timetable.js';
|
|
50
56
|
import { SequencerState, type SequencerStateWithSlot } from './utils.js';
|
|
51
57
|
|
|
52
58
|
export { SequencerState };
|
|
@@ -58,7 +64,7 @@ export type SequencerEvents = {
|
|
|
58
64
|
oldState: SequencerState;
|
|
59
65
|
newState: SequencerState;
|
|
60
66
|
secondsIntoSlot?: number;
|
|
61
|
-
slotNumber?:
|
|
67
|
+
slotNumber?: SlotNumber;
|
|
62
68
|
}) => void;
|
|
63
69
|
['proposer-rollup-check-failed']: (args: { reason: string }) => void;
|
|
64
70
|
['tx-count-check-failed']: (args: { minTxs: number; availableTxs: number }) => void;
|
|
@@ -69,7 +75,7 @@ export type SequencerEvents = {
|
|
|
69
75
|
sentActions?: Action[];
|
|
70
76
|
expiredActions?: Action[];
|
|
71
77
|
}) => void;
|
|
72
|
-
['block-published']: (args: { blockNumber:
|
|
78
|
+
['block-published']: (args: { blockNumber: BlockNumber; slot: number }) => void;
|
|
73
79
|
};
|
|
74
80
|
|
|
75
81
|
/**
|
|
@@ -96,6 +102,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
96
102
|
|
|
97
103
|
private governanceProposerPayload: EthAddress | undefined;
|
|
98
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
|
+
|
|
99
111
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
|
|
100
112
|
protected timetable!: SequencerTimetable;
|
|
101
113
|
protected enforceTimeTable: boolean = false;
|
|
@@ -127,15 +139,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
127
139
|
) {
|
|
128
140
|
super();
|
|
129
141
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
? EthAddress.ZERO
|
|
135
|
-
: (this.validatorClient?.getCoinbaseForAttestor(validatorAddresses[0]) ?? EthAddress.ZERO);
|
|
136
|
-
|
|
137
|
-
this.metrics = new SequencerMetrics(telemetry, () => this.state, coinbase, this.rollupContract, 'Sequencer');
|
|
142
|
+
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
143
|
+
if (this.config.fishermanMode) {
|
|
144
|
+
this.log = log.createChild('[FISHERMAN]');
|
|
145
|
+
}
|
|
138
146
|
|
|
147
|
+
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
139
148
|
// Initialize config
|
|
140
149
|
this.updateConfig(this.config);
|
|
141
150
|
}
|
|
@@ -213,6 +222,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
213
222
|
}
|
|
214
223
|
|
|
215
224
|
public async init() {
|
|
225
|
+
// Takes ~3s to precompute some tables.
|
|
226
|
+
getKzg();
|
|
216
227
|
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
217
228
|
}
|
|
218
229
|
|
|
@@ -220,8 +231,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
220
231
|
* Starts the sequencer and moves to IDLE state.
|
|
221
232
|
*/
|
|
222
233
|
public start() {
|
|
223
|
-
this.
|
|
224
|
-
this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
|
|
234
|
+
this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
|
|
225
235
|
this.setState(SequencerState.IDLE, undefined, { force: true });
|
|
226
236
|
this.runningPromise.start();
|
|
227
237
|
this.log.info('Started sequencer');
|
|
@@ -232,9 +242,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
232
242
|
*/
|
|
233
243
|
public async stop(): Promise<void> {
|
|
234
244
|
this.log.info(`Stopping sequencer`);
|
|
235
|
-
this.
|
|
245
|
+
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
236
246
|
this.publisher?.interrupt();
|
|
237
|
-
await this.validatorClient?.stop();
|
|
238
247
|
await this.runningPromise?.stop();
|
|
239
248
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
240
249
|
this.log.info('Stopped sequencer');
|
|
@@ -256,27 +265,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
256
265
|
* - Submit block
|
|
257
266
|
* - If our block for some reason is not included, revert the state
|
|
258
267
|
*/
|
|
259
|
-
protected async
|
|
268
|
+
protected async work() {
|
|
260
269
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
270
|
+
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
261
271
|
|
|
262
|
-
// Check
|
|
263
|
-
|
|
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
|
+
}
|
|
264
279
|
|
|
265
|
-
//
|
|
280
|
+
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
281
|
+
const syncedTo = await this.checkSync({ ts, slot });
|
|
266
282
|
if (!syncedTo) {
|
|
283
|
+
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
267
284
|
return;
|
|
268
285
|
}
|
|
269
286
|
|
|
270
287
|
const chainTipArchive = syncedTo.archive;
|
|
271
|
-
const newBlockNumber = syncedTo.blockNumber + 1;
|
|
288
|
+
const newBlockNumber = BlockNumber(syncedTo.blockNumber + 1);
|
|
272
289
|
|
|
273
|
-
const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
274
|
-
|
|
275
|
-
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
276
|
-
|
|
277
|
-
// Check that the archiver and dependencies have synced to the previous L1 slot at least
|
|
278
|
-
// TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
|
|
279
|
-
// cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
|
|
280
290
|
const syncLogData = {
|
|
281
291
|
now,
|
|
282
292
|
syncedToL1Ts: syncedTo.l1Timestamp,
|
|
@@ -288,86 +298,70 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
288
298
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
289
299
|
};
|
|
290
300
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
syncLogData,
|
|
295
|
-
);
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Check that the slot is not taken by a block already
|
|
300
|
-
if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
|
|
301
|
-
this.log.debug(
|
|
302
|
-
`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
303
|
-
{ ...syncLogData, block: syncedTo.block.header.toInspect() },
|
|
304
|
-
);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
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);
|
|
307
304
|
|
|
308
|
-
//
|
|
309
|
-
if (
|
|
310
|
-
this.
|
|
311
|
-
`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
|
|
312
|
-
{ ...syncLogData, block: this.lastBlockPublished.header.toInspect() },
|
|
313
|
-
);
|
|
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);
|
|
314
308
|
return;
|
|
315
309
|
}
|
|
316
310
|
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
} catch (e) {
|
|
322
|
-
if (e instanceof NoCommitteeError) {
|
|
323
|
-
this.log.warn(
|
|
324
|
-
`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`,
|
|
325
|
-
);
|
|
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 });
|
|
326
315
|
return;
|
|
327
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;
|
|
328
323
|
}
|
|
329
324
|
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
...syncLogData,
|
|
338
|
-
});
|
|
339
|
-
// If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
|
|
340
|
-
if (!syncedTo.pendingChainValidationStatus.valid) {
|
|
341
|
-
// We pass i undefined here to get any available publisher.
|
|
342
|
-
const { publisher } = await this.publisherFactory.create(undefined);
|
|
343
|
-
await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
|
|
344
|
-
}
|
|
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');
|
|
345
332
|
return;
|
|
346
333
|
}
|
|
347
334
|
|
|
348
|
-
// Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
|
|
349
|
-
// if all the previous checks are good, but we do it just in case.
|
|
350
|
-
const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
|
|
351
|
-
|
|
352
335
|
// We now need to get ourselves a publisher.
|
|
353
336
|
// The returned attestor will be the one we provided if we provided one.
|
|
354
337
|
// Otherwise it will be a valid attestor for the returned publisher.
|
|
355
|
-
|
|
356
|
-
|
|
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
|
+
);
|
|
357
342
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
358
|
-
|
|
359
343
|
this.publisher = publisher;
|
|
360
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
|
|
361
354
|
const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
|
|
362
355
|
const feeRecipient = this.validatorClient!.getFeeRecipientForAttestor(attestorAddress);
|
|
363
356
|
|
|
364
|
-
this.metrics.setCoinbase(coinbase);
|
|
365
|
-
|
|
366
357
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
367
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.
|
|
368
362
|
const canProposeCheck = await publisher.canProposeAtNextEthBlock(
|
|
369
363
|
chainTipArchive,
|
|
370
|
-
|
|
364
|
+
proposer ?? EthAddress.ZERO,
|
|
371
365
|
invalidateBlock,
|
|
372
366
|
);
|
|
373
367
|
|
|
@@ -377,6 +371,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
377
371
|
syncLogData,
|
|
378
372
|
);
|
|
379
373
|
this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed' });
|
|
374
|
+
this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
|
|
380
375
|
return;
|
|
381
376
|
} else if (canProposeCheck.slot !== slot) {
|
|
382
377
|
this.log.warn(
|
|
@@ -384,20 +379,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
384
379
|
{ ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
|
|
385
380
|
);
|
|
386
381
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch' });
|
|
382
|
+
this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
|
|
387
383
|
return;
|
|
388
|
-
} else if (canProposeCheck.
|
|
384
|
+
} else if (canProposeCheck.checkpointNumber !== CheckpointNumber.fromBlockNumber(newBlockNumber)) {
|
|
389
385
|
this.log.warn(
|
|
390
|
-
`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}.`,
|
|
391
387
|
{ ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
|
|
392
388
|
);
|
|
393
389
|
this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch' });
|
|
390
|
+
this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
|
|
394
391
|
return;
|
|
395
392
|
}
|
|
396
393
|
|
|
397
|
-
this.log.debug(
|
|
398
|
-
`Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
|
|
399
|
-
{ ...syncLogData, validatorAddresses },
|
|
400
|
-
);
|
|
394
|
+
this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, { ...syncLogData });
|
|
401
395
|
|
|
402
396
|
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(
|
|
403
397
|
newBlockNumber,
|
|
@@ -406,51 +400,98 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
406
400
|
slot,
|
|
407
401
|
);
|
|
408
402
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
.catch(err => {
|
|
418
|
-
this.log.error(`Error enqueuing governance vote`, err, { blockNumber: newBlockNumber, slot });
|
|
419
|
-
return false;
|
|
420
|
-
})
|
|
421
|
-
: Promise.resolve(false);
|
|
422
|
-
|
|
423
|
-
const enqueueSlashingActionsPromise = this.slasherClient
|
|
424
|
-
? this.slasherClient
|
|
425
|
-
.getProposerActions(slot)
|
|
426
|
-
.then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
|
|
427
|
-
.catch(err => {
|
|
428
|
-
this.log.error(`Error enqueuing slashing actions`, err, { blockNumber: newBlockNumber, slot });
|
|
429
|
-
return false;
|
|
430
|
-
})
|
|
431
|
-
: 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
|
+
);
|
|
432
411
|
|
|
412
|
+
// Enqueues block invalidation
|
|
433
413
|
if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
|
|
434
414
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
435
415
|
}
|
|
436
416
|
|
|
417
|
+
// Actual block building
|
|
437
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
|
+
);
|
|
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
|
+
) {
|
|
438
481
|
this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
|
|
439
|
-
proposer
|
|
440
|
-
coinbase,
|
|
482
|
+
proposer,
|
|
441
483
|
publisher: publisher.getSenderAddress(),
|
|
442
|
-
feeRecipient,
|
|
443
484
|
globalVariables: newGlobalVariables.toInspect(),
|
|
444
485
|
chainTipArchive,
|
|
445
486
|
blockNumber: newBlockNumber,
|
|
446
487
|
slot,
|
|
447
488
|
});
|
|
448
489
|
|
|
449
|
-
|
|
450
|
-
const proposalHeader = ProposedBlockHeader.from({
|
|
490
|
+
const proposalHeader = CheckpointHeader.from({
|
|
451
491
|
...newGlobalVariables,
|
|
452
492
|
timestamp: newGlobalVariables.timestamp,
|
|
453
493
|
lastArchiveRoot: chainTipArchive,
|
|
494
|
+
blockHeadersHash: Fr.ZERO,
|
|
454
495
|
contentCommitment: ContentCommitment.empty(),
|
|
455
496
|
totalManaUsed: Fr.ZERO,
|
|
456
497
|
});
|
|
@@ -467,7 +508,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
467
508
|
pendingTxs,
|
|
468
509
|
proposalHeader,
|
|
469
510
|
newGlobalVariables,
|
|
470
|
-
|
|
511
|
+
proposer,
|
|
471
512
|
invalidateBlock,
|
|
472
513
|
publisher,
|
|
473
514
|
);
|
|
@@ -478,6 +519,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
478
519
|
} else {
|
|
479
520
|
this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
|
|
480
521
|
}
|
|
522
|
+
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
481
523
|
}
|
|
482
524
|
} else {
|
|
483
525
|
this.log.verbose(
|
|
@@ -485,27 +527,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
485
527
|
{ chainTipArchive, blockNumber: newBlockNumber, slot },
|
|
486
528
|
);
|
|
487
529
|
this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
|
|
530
|
+
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
488
531
|
}
|
|
489
|
-
|
|
490
|
-
await Promise.all([enqueueGovernanceSignalPromise, enqueueSlashingActionsPromise]);
|
|
491
|
-
|
|
492
|
-
const l1Response = await publisher.sendRequests();
|
|
493
|
-
const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
|
|
494
|
-
if (proposedBlock) {
|
|
495
|
-
this.lastBlockPublished = block;
|
|
496
|
-
this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
|
|
497
|
-
this.metrics.incFilledSlot(publisher.getSenderAddress().toString());
|
|
498
|
-
} else if (block) {
|
|
499
|
-
this.emit('block-publish-failed', l1Response ?? {});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
this.setState(SequencerState.IDLE, undefined);
|
|
532
|
+
return block;
|
|
503
533
|
}
|
|
504
534
|
|
|
505
535
|
@trackSpan('Sequencer.work')
|
|
506
|
-
protected async
|
|
536
|
+
protected async safeWork() {
|
|
507
537
|
try {
|
|
508
|
-
await this.
|
|
538
|
+
await this.work();
|
|
509
539
|
} catch (err) {
|
|
510
540
|
if (err instanceof SequencerTooSlowError) {
|
|
511
541
|
// Log as warn only if we had to abort halfway through the block proposal
|
|
@@ -528,13 +558,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
528
558
|
* @param slotNumber - The current slot number.
|
|
529
559
|
* @param force - Whether to force the transition even if the sequencer is stopped.
|
|
530
560
|
*/
|
|
531
|
-
setState(proposedState: SequencerStateWithSlot, slotNumber:
|
|
561
|
+
setState(proposedState: SequencerStateWithSlot, slotNumber: SlotNumber, opts?: { force?: boolean }): void;
|
|
532
562
|
setState(
|
|
533
563
|
proposedState: Exclude<SequencerState, SequencerStateWithSlot>,
|
|
534
564
|
slotNumber?: undefined,
|
|
535
565
|
opts?: { force?: boolean },
|
|
536
566
|
): void;
|
|
537
|
-
setState(proposedState: SequencerState, slotNumber:
|
|
567
|
+
setState(proposedState: SequencerState, slotNumber: SlotNumber | undefined, opts: { force?: boolean } = {}): void {
|
|
568
|
+
if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
|
|
569
|
+
this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
|
|
570
|
+
throw new SequencerInterruptedError();
|
|
571
|
+
}
|
|
538
572
|
if (this.state === SequencerState.STOPPED && !opts.force) {
|
|
539
573
|
this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
|
|
540
574
|
return;
|
|
@@ -545,7 +579,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
545
579
|
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
546
580
|
}
|
|
547
581
|
|
|
548
|
-
|
|
582
|
+
const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
|
|
583
|
+
const logLevel =
|
|
584
|
+
boringStates.includes(proposedState) && boringStates.includes(this.state)
|
|
585
|
+
? ('trace' as const)
|
|
586
|
+
: ('debug' as const);
|
|
587
|
+
this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, { slotNumber, secondsIntoSlot });
|
|
588
|
+
|
|
549
589
|
this.emit('state-changed', {
|
|
550
590
|
oldState: this.state,
|
|
551
591
|
newState: proposedState,
|
|
@@ -565,7 +605,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
565
605
|
await this.p2pClient.deleteTxs(failedTxHashes);
|
|
566
606
|
}
|
|
567
607
|
|
|
568
|
-
protected getBlockBuilderOptions(slot:
|
|
608
|
+
protected getBlockBuilderOptions(slot: SlotNumber): PublicProcessorLimits {
|
|
569
609
|
// Deadline for processing depends on whether we're proposing a block
|
|
570
610
|
const secondsIntoSlot = this.getSecondsIntoSlot(slot);
|
|
571
611
|
const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
|
|
@@ -578,6 +618,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
578
618
|
maxTransactions: this.maxTxsPerBlock,
|
|
579
619
|
maxBlockSize: this.maxBlockSizeInBytes,
|
|
580
620
|
maxBlockGas: this.maxBlockGas,
|
|
621
|
+
maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
|
|
581
622
|
deadline,
|
|
582
623
|
};
|
|
583
624
|
}
|
|
@@ -598,7 +639,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
598
639
|
}))
|
|
599
640
|
private async buildBlockAndEnqueuePublish(
|
|
600
641
|
pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
|
|
601
|
-
proposalHeader:
|
|
642
|
+
proposalHeader: CheckpointHeader,
|
|
602
643
|
newGlobalVariables: GlobalVariables,
|
|
603
644
|
proposerAddress: EthAddress | undefined,
|
|
604
645
|
invalidateBlock: InvalidateBlockRequest | undefined,
|
|
@@ -607,15 +648,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
607
648
|
await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
|
|
608
649
|
|
|
609
650
|
const blockNumber = newGlobalVariables.blockNumber;
|
|
610
|
-
const
|
|
611
|
-
const
|
|
651
|
+
const checkpointNumber = CheckpointNumber.fromBlockNumber(blockNumber);
|
|
652
|
+
const slot = proposalHeader.slotNumber;
|
|
653
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
612
654
|
|
|
613
|
-
// this.metrics.recordNewBlock(blockNumber, validTxs.length);
|
|
614
655
|
const workTimer = new Timer();
|
|
615
656
|
this.setState(SequencerState.CREATING_BLOCK, slot);
|
|
616
657
|
|
|
617
658
|
try {
|
|
618
|
-
const blockBuilderOptions = this.getBlockBuilderOptions(
|
|
659
|
+
const blockBuilderOptions = this.getBlockBuilderOptions(slot);
|
|
619
660
|
const buildBlockRes = await this.blockBuilder.buildBlock(
|
|
620
661
|
pendingTxs,
|
|
621
662
|
l1ToL2Messages,
|
|
@@ -638,7 +679,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
638
679
|
|
|
639
680
|
// TODO(@PhilWindle) We should probably periodically check for things like another
|
|
640
681
|
// block being published before ours instead of just waiting on our block
|
|
641
|
-
await publisher.validateBlockHeader(block.
|
|
682
|
+
await publisher.validateBlockHeader(block.getCheckpointHeader(), invalidateBlock);
|
|
642
683
|
|
|
643
684
|
const blockStats: L2BlockBuiltStats = {
|
|
644
685
|
eventName: 'l2-block-built',
|
|
@@ -663,13 +704,36 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
663
704
|
},
|
|
664
705
|
);
|
|
665
706
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (
|
|
669
|
-
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
|
+
);
|
|
670
719
|
}
|
|
671
720
|
|
|
672
|
-
|
|
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
|
+
);
|
|
729
|
+
|
|
730
|
+
await this.enqueuePublishL2Block(
|
|
731
|
+
block,
|
|
732
|
+
attestationsAndSigners,
|
|
733
|
+
attestationsAndSignersSignature,
|
|
734
|
+
invalidateBlock,
|
|
735
|
+
publisher,
|
|
736
|
+
);
|
|
673
737
|
this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
|
|
674
738
|
return block;
|
|
675
739
|
} catch (err) {
|
|
@@ -687,8 +751,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
687
751
|
block: L2Block,
|
|
688
752
|
txs: Tx[],
|
|
689
753
|
proposerAddress: EthAddress | undefined,
|
|
690
|
-
): Promise<
|
|
691
|
-
const { committee } = await this.epochCache.getCommittee(block.
|
|
754
|
+
): Promise<CommitteeAttestationsAndSigners> {
|
|
755
|
+
const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
|
|
692
756
|
|
|
693
757
|
// We checked above that the committee is defined, so this should never happen.
|
|
694
758
|
if (!committee) {
|
|
@@ -697,29 +761,29 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
697
761
|
|
|
698
762
|
if (committee.length === 0) {
|
|
699
763
|
this.log.verbose(`Attesting committee is empty`);
|
|
700
|
-
return
|
|
764
|
+
return CommitteeAttestationsAndSigners.empty();
|
|
701
765
|
} else {
|
|
702
766
|
this.log.debug(`Attesting committee length is ${committee.length}`);
|
|
703
767
|
}
|
|
704
768
|
|
|
705
769
|
if (!this.validatorClient) {
|
|
706
|
-
|
|
707
|
-
this.log.error(msg);
|
|
708
|
-
throw new Error(msg);
|
|
770
|
+
throw new Error('Missing validator client: Cannot collect attestations');
|
|
709
771
|
}
|
|
710
772
|
|
|
711
773
|
const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
|
|
712
774
|
|
|
713
|
-
const slotNumber = block.header.globalVariables.slotNumber
|
|
775
|
+
const slotNumber = block.header.globalVariables.slotNumber;
|
|
714
776
|
this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
|
|
715
777
|
|
|
716
778
|
this.log.debug('Creating block proposal for validators');
|
|
717
|
-
const blockProposalOptions: BlockProposalOptions = {
|
|
779
|
+
const blockProposalOptions: BlockProposalOptions = {
|
|
780
|
+
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
781
|
+
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
782
|
+
};
|
|
718
783
|
const proposal = await this.validatorClient.createBlockProposal(
|
|
719
784
|
block.header.globalVariables.blockNumber,
|
|
720
|
-
block.
|
|
785
|
+
block.getCheckpointHeader(),
|
|
721
786
|
block.archive.root,
|
|
722
|
-
block.header.state,
|
|
723
787
|
txs,
|
|
724
788
|
proposerAddress,
|
|
725
789
|
blockProposalOptions,
|
|
@@ -732,7 +796,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
732
796
|
if (this.config.skipCollectingAttestations) {
|
|
733
797
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
734
798
|
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
735
|
-
return orderAttestations(attestations ?? [], committee);
|
|
799
|
+
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
736
800
|
}
|
|
737
801
|
|
|
738
802
|
this.log.debug('Broadcasting block proposal to validators');
|
|
@@ -745,7 +809,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
745
809
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
746
810
|
|
|
747
811
|
const timer = new Timer();
|
|
748
|
-
let
|
|
812
|
+
let collectedAttestationsCount: number = 0;
|
|
749
813
|
try {
|
|
750
814
|
const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
|
|
751
815
|
const attestations = await this.validatorClient.collectAttestations(
|
|
@@ -754,20 +818,74 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
754
818
|
attestationDeadline,
|
|
755
819
|
);
|
|
756
820
|
|
|
757
|
-
|
|
821
|
+
collectedAttestationsCount = attestations.length;
|
|
758
822
|
|
|
759
823
|
// note: the smart contract requires that the signatures are provided in the order of the committee
|
|
760
|
-
|
|
824
|
+
const sorted = orderAttestations(attestations, committee);
|
|
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);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return new CommitteeAttestationsAndSigners(sorted);
|
|
761
832
|
} catch (err) {
|
|
762
833
|
if (err && err instanceof AttestationTimeoutError) {
|
|
763
|
-
|
|
834
|
+
collectedAttestationsCount = err.collectedCount;
|
|
764
835
|
}
|
|
765
836
|
throw err;
|
|
766
837
|
} finally {
|
|
767
|
-
this.metrics.recordCollectedAttestations(
|
|
838
|
+
this.metrics.recordCollectedAttestations(collectedAttestationsCount, timer.ms());
|
|
768
839
|
}
|
|
769
840
|
}
|
|
770
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
|
+
|
|
771
889
|
/**
|
|
772
890
|
* Publishes the L2Block to the rollup contract.
|
|
773
891
|
* @param block - The L2Block to be published.
|
|
@@ -777,22 +895,27 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
777
895
|
}))
|
|
778
896
|
protected async enqueuePublishL2Block(
|
|
779
897
|
block: L2Block,
|
|
780
|
-
|
|
781
|
-
|
|
898
|
+
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
899
|
+
attestationsAndSignersSignature: Signature,
|
|
782
900
|
invalidateBlock: InvalidateBlockRequest | undefined,
|
|
783
901
|
publisher: SequencerPublisher,
|
|
784
902
|
): Promise<void> {
|
|
785
903
|
// Publishes new block to the network and awaits the tx to be mined
|
|
786
|
-
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber
|
|
904
|
+
this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
|
|
787
905
|
|
|
788
906
|
// Time out tx at the end of the slot
|
|
789
|
-
const slot = block.header.globalVariables.slotNumber
|
|
907
|
+
const slot = block.header.globalVariables.slotNumber;
|
|
790
908
|
const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
|
|
791
909
|
|
|
792
|
-
const enqueued = await publisher.enqueueProposeL2Block(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
910
|
+
const enqueued = await publisher.enqueueProposeL2Block(
|
|
911
|
+
block,
|
|
912
|
+
attestationsAndSigners,
|
|
913
|
+
attestationsAndSignersSignature,
|
|
914
|
+
{
|
|
915
|
+
txTimeoutAt,
|
|
916
|
+
forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber,
|
|
917
|
+
},
|
|
918
|
+
);
|
|
796
919
|
|
|
797
920
|
if (!enqueued) {
|
|
798
921
|
throw new Error(`Failed to enqueue publish of block ${block.number}`);
|
|
@@ -802,18 +925,31 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
802
925
|
/**
|
|
803
926
|
* Returns whether all dependencies have caught up.
|
|
804
927
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
805
|
-
* @returns Boolean indicating if our dependencies are synced to the latest block.
|
|
806
928
|
*/
|
|
807
|
-
protected async
|
|
929
|
+
protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<
|
|
808
930
|
| {
|
|
809
931
|
block?: L2Block;
|
|
810
|
-
blockNumber:
|
|
932
|
+
blockNumber: BlockNumber;
|
|
811
933
|
archive: Fr;
|
|
812
934
|
l1Timestamp: bigint;
|
|
813
935
|
pendingChainValidationStatus: ValidateBlockResult;
|
|
814
936
|
}
|
|
815
937
|
| undefined
|
|
816
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
|
+
|
|
817
953
|
const syncedBlocks = await Promise.all([
|
|
818
954
|
this.worldState.status().then(({ syncSummary }) => ({
|
|
819
955
|
number: syncSummary.latestBlockNumber,
|
|
@@ -822,49 +958,203 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
822
958
|
this.l2BlockSource.getL2Tips().then(t => t.latest),
|
|
823
959
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
824
960
|
this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
|
|
825
|
-
this.l2BlockSource.getL1Timestamp(),
|
|
826
961
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
827
962
|
] as const);
|
|
828
963
|
|
|
829
|
-
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource,
|
|
830
|
-
syncedBlocks;
|
|
964
|
+
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
|
|
831
965
|
|
|
832
|
-
//
|
|
833
|
-
//
|
|
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.
|
|
834
968
|
const result =
|
|
835
|
-
l2BlockSource.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
l1ToL2MessageSource.hash === l2BlockSource.hash;
|
|
840
|
-
|
|
841
|
-
const logData = { worldState, l2BlockSource, p2p, l1ToL2MessageSource };
|
|
842
|
-
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);
|
|
843
973
|
|
|
844
974
|
if (!result) {
|
|
975
|
+
this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
|
|
845
976
|
return undefined;
|
|
846
977
|
}
|
|
847
978
|
|
|
979
|
+
// Special case for genesis state
|
|
848
980
|
const blockNumber = worldState.number;
|
|
849
|
-
if (blockNumber
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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];
|
|
855
1072
|
}
|
|
1073
|
+
this.log.error(`Error getting proposer for slot ${slot}`, e);
|
|
1074
|
+
return [false, undefined];
|
|
1075
|
+
}
|
|
856
1076
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
|
|
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;
|
|
867
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
|
|
1157
|
+
await publisher.sendRequests();
|
|
868
1158
|
}
|
|
869
1159
|
|
|
870
1160
|
/**
|
|
@@ -874,27 +1164,26 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
874
1164
|
* and if they fail, any sequencer will try as well.
|
|
875
1165
|
*/
|
|
876
1166
|
protected async considerInvalidatingBlock(
|
|
877
|
-
syncedTo: NonNullable<Awaited<ReturnType<Sequencer['
|
|
878
|
-
currentSlot:
|
|
879
|
-
ourValidatorAddresses: EthAddress[],
|
|
880
|
-
publisher: SequencerPublisher,
|
|
1167
|
+
syncedTo: NonNullable<Awaited<ReturnType<Sequencer['checkSync']>>>,
|
|
1168
|
+
currentSlot: SlotNumber,
|
|
881
1169
|
): Promise<void> {
|
|
882
1170
|
const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
|
|
883
1171
|
if (pendingChainValidationStatus.valid) {
|
|
884
1172
|
return;
|
|
885
1173
|
}
|
|
886
1174
|
|
|
887
|
-
const
|
|
888
|
-
const
|
|
889
|
-
const
|
|
1175
|
+
const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
|
|
1176
|
+
const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
|
|
1177
|
+
const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
|
|
1178
|
+
const ourValidatorAddresses = this.validatorClient!.getValidatorAddresses();
|
|
890
1179
|
|
|
891
1180
|
const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
|
|
892
1181
|
this.config;
|
|
893
1182
|
|
|
894
1183
|
const logData = {
|
|
895
|
-
invalidL1Timestamp,
|
|
1184
|
+
invalidL1Timestamp: invalidBlockTimestamp,
|
|
896
1185
|
l1Timestamp,
|
|
897
|
-
invalidBlock: pendingChainValidationStatus.block
|
|
1186
|
+
invalidBlock: pendingChainValidationStatus.block,
|
|
898
1187
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
899
1188
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
900
1189
|
ourValidatorAddresses,
|
|
@@ -922,6 +1211,24 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
922
1211
|
return;
|
|
923
1212
|
}
|
|
924
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
|
+
|
|
925
1232
|
const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
|
|
926
1233
|
if (!invalidateBlock) {
|
|
927
1234
|
this.log.warn(`Failed to simulate invalidate block`, logData);
|
|
@@ -936,18 +1243,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
936
1243
|
);
|
|
937
1244
|
|
|
938
1245
|
publisher.enqueueInvalidateBlock(invalidateBlock);
|
|
939
|
-
|
|
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
|
+
}
|
|
940
1253
|
}
|
|
941
1254
|
|
|
942
|
-
private getSlotStartBuildTimestamp(slotNumber:
|
|
943
|
-
return (
|
|
944
|
-
Number(this.l1Constants.l1GenesisTime) +
|
|
945
|
-
Number(slotNumber) * this.l1Constants.slotDuration -
|
|
946
|
-
this.l1Constants.ethereumSlotDuration
|
|
947
|
-
);
|
|
1255
|
+
private getSlotStartBuildTimestamp(slotNumber: SlotNumber): number {
|
|
1256
|
+
return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
|
|
948
1257
|
}
|
|
949
1258
|
|
|
950
|
-
private getSecondsIntoSlot(slotNumber:
|
|
1259
|
+
private getSecondsIntoSlot(slotNumber: SlotNumber): number {
|
|
951
1260
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
|
|
952
1261
|
return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
|
|
953
1262
|
}
|