@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.
Files changed (73) hide show
  1. package/dest/client/index.d.ts +1 -1
  2. package/dest/client/sequencer-client.d.ts +7 -6
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +23 -14
  5. package/dest/config.d.ts +5 -3
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +21 -1
  8. package/dest/global_variable_builder/global_builder.d.ts +5 -7
  9. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  10. package/dest/global_variable_builder/global_builder.js +12 -8
  11. package/dest/global_variable_builder/index.d.ts +1 -1
  12. package/dest/index.d.ts +1 -1
  13. package/dest/publisher/config.d.ts +9 -10
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +19 -17
  16. package/dest/publisher/index.d.ts +2 -2
  17. package/dest/publisher/index.d.ts.map +1 -1
  18. package/dest/publisher/index.js +1 -1
  19. package/dest/publisher/sequencer-publisher-factory.d.ts +9 -3
  20. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher-factory.js +8 -1
  22. package/dest/publisher/sequencer-publisher-metrics.d.ts +2 -2
  23. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher-metrics.js +1 -1
  25. package/dest/publisher/sequencer-publisher.d.ts +53 -50
  26. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  27. package/dest/publisher/sequencer-publisher.js +187 -132
  28. package/dest/sequencer/block_builder.d.ts +6 -8
  29. package/dest/sequencer/block_builder.d.ts.map +1 -1
  30. package/dest/sequencer/block_builder.js +18 -9
  31. package/dest/sequencer/config.d.ts +2 -2
  32. package/dest/sequencer/config.d.ts.map +1 -1
  33. package/dest/sequencer/errors.d.ts +11 -0
  34. package/dest/sequencer/errors.d.ts.map +1 -0
  35. package/dest/sequencer/errors.js +15 -0
  36. package/dest/sequencer/index.d.ts +1 -1
  37. package/dest/sequencer/metrics.d.ts +17 -20
  38. package/dest/sequencer/metrics.d.ts.map +1 -1
  39. package/dest/sequencer/metrics.js +59 -87
  40. package/dest/sequencer/sequencer.d.ts +54 -32
  41. package/dest/sequencer/sequencer.d.ts.map +1 -1
  42. package/dest/sequencer/sequencer.js +458 -184
  43. package/dest/sequencer/timetable.d.ts +4 -8
  44. package/dest/sequencer/timetable.d.ts.map +1 -1
  45. package/dest/sequencer/timetable.js +3 -10
  46. package/dest/sequencer/utils.d.ts +11 -25
  47. package/dest/sequencer/utils.d.ts.map +1 -1
  48. package/dest/sequencer/utils.js +9 -24
  49. package/dest/test/index.d.ts +2 -2
  50. package/dest/test/index.d.ts.map +1 -1
  51. package/dest/tx_validator/nullifier_cache.d.ts +1 -1
  52. package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
  53. package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
  54. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  55. package/dest/tx_validator/tx_validator_factory.js +12 -9
  56. package/package.json +32 -31
  57. package/src/client/sequencer-client.ts +24 -18
  58. package/src/config.ts +23 -6
  59. package/src/global_variable_builder/global_builder.ts +19 -17
  60. package/src/publisher/config.ts +29 -27
  61. package/src/publisher/index.ts +1 -1
  62. package/src/publisher/sequencer-publisher-factory.ts +16 -3
  63. package/src/publisher/sequencer-publisher-metrics.ts +1 -1
  64. package/src/publisher/sequencer-publisher.ts +257 -183
  65. package/src/sequencer/block_builder.ts +23 -28
  66. package/src/sequencer/config.ts +1 -1
  67. package/src/sequencer/errors.ts +21 -0
  68. package/src/sequencer/metrics.ts +71 -98
  69. package/src/sequencer/sequencer.ts +546 -237
  70. package/src/sequencer/timetable.ts +10 -14
  71. package/src/sequencer/utils.ts +10 -24
  72. package/src/test/index.ts +1 -1
  73. package/src/tx_validator/tx_validator_factory.ts +13 -7
@@ -4,26 +4,35 @@ function _ts_decorate(decorators, target, key, desc) {
4
4
  else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  }
7
- import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
8
- import { FormattedViemError, NoCommitteeError } from '@aztec/ethereum';
7
+ import { getKzg } from '@aztec/blob-lib';
8
+ import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
9
+ import { NoCommitteeError } from '@aztec/ethereum/contracts';
10
+ import { FormattedViemError } from '@aztec/ethereum/utils';
11
+ import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
9
12
  import { omit, pick } from '@aztec/foundation/collection';
13
+ import { randomInt } from '@aztec/foundation/crypto/random';
14
+ import { Fr } from '@aztec/foundation/curves/bn254';
10
15
  import { EthAddress } from '@aztec/foundation/eth-address';
11
- import { Fr } from '@aztec/foundation/fields';
16
+ import { Signature } from '@aztec/foundation/eth-signature';
12
17
  import { createLogger } from '@aztec/foundation/log';
13
18
  import { RunningPromise } from '@aztec/foundation/running-promise';
14
19
  import { Timer } from '@aztec/foundation/timer';
15
- import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
20
+ import { unfreeze } from '@aztec/foundation/types';
21
+ import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
22
+ import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
16
23
  import { Gas } from '@aztec/stdlib/gas';
17
24
  import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
18
25
  import { orderAttestations } from '@aztec/stdlib/p2p';
26
+ import { CheckpointHeader } from '@aztec/stdlib/rollup';
19
27
  import { pickFromSchema } from '@aztec/stdlib/schemas';
20
28
  import { MerkleTreeId } from '@aztec/stdlib/trees';
21
- import { ContentCommitment, ProposedBlockHeader } from '@aztec/stdlib/tx';
29
+ import { ContentCommitment } from '@aztec/stdlib/tx';
22
30
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
23
31
  import { Attributes, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
24
32
  import EventEmitter from 'node:events';
33
+ import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
25
34
  import { SequencerMetrics } from './metrics.js';
26
- import { SequencerTimetable, SequencerTooSlowError } from './timetable.js';
35
+ import { SequencerTimetable } from './timetable.js';
27
36
  import { SequencerState } from './utils.js';
28
37
  export { SequencerState };
29
38
  /**
@@ -62,6 +71,8 @@ export { SequencerState };
62
71
  metrics;
63
72
  lastBlockPublished;
64
73
  governanceProposerPayload;
74
+ /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */ lastSlotForVoteWhenSyncFailed;
75
+ /** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */ lastSlotForValidationBlock;
65
76
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
66
77
  enforceTimeTable;
67
78
  // This shouldn't be here as this gets re-created each time we build/propose a block.
@@ -72,10 +83,11 @@ export { SequencerState };
72
83
  publisher;
73
84
  constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
74
85
  super(), this.publisherFactory = publisherFactory, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.blockBuilder = blockBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.epochCache = epochCache, this.rollupContract = rollupContract, this.config = config, this.telemetry = telemetry, this.log = log, this.pollingIntervalMs = 1000, this.maxTxsPerBlock = 32, this.minTxsPerBlock = 1, this.maxL1TxInclusionTimeIntoSlot = 0, this.state = SequencerState.STOPPED, this.maxBlockSizeInBytes = 1024 * 1024, this.maxBlockGas = new Gas(100e9, 100e9), this.enforceTimeTable = false;
75
- // Set an initial coinbase for metrics purposes, but this will potentially change with each block.
76
- const validatorAddresses = this.validatorClient?.getValidatorAddresses() ?? [];
77
- const coinbase = validatorAddresses.length === 0 ? EthAddress.ZERO : this.validatorClient?.getCoinbaseForAttestor(validatorAddresses[0]) ?? EthAddress.ZERO;
78
- this.metrics = new SequencerMetrics(telemetry, ()=>this.state, coinbase, this.rollupContract, 'Sequencer');
86
+ // Add [FISHERMAN] prefix to logger if in fisherman mode
87
+ if (this.config.fishermanMode) {
88
+ this.log = log.createChild('[FISHERMAN]');
89
+ }
90
+ this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
79
91
  // Initialize config
80
92
  this.updateConfig(this.config);
81
93
  }
@@ -135,13 +147,14 @@ export { SequencerState };
135
147
  }, this.metrics, this.log);
136
148
  }
137
149
  async init() {
150
+ // Takes ~3s to precompute some tables.
151
+ getKzg();
138
152
  this.publisher = (await this.publisherFactory.create(undefined)).publisher;
139
153
  }
140
154
  /**
141
155
  * Starts the sequencer and moves to IDLE state.
142
156
  */ start() {
143
- this.metrics.start();
144
- this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
157
+ this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
145
158
  this.setState(SequencerState.IDLE, undefined, {
146
159
  force: true
147
160
  });
@@ -152,9 +165,10 @@ export { SequencerState };
152
165
  * Stops the sequencer from processing txs and moves to STOPPED state.
153
166
  */ async stop() {
154
167
  this.log.info(`Stopping sequencer`);
155
- this.metrics.stop();
168
+ this.setState(SequencerState.STOPPING, undefined, {
169
+ force: true
170
+ });
156
171
  this.publisher?.interrupt();
157
- await this.validatorClient?.stop();
158
172
  await this.runningPromise?.stop();
159
173
  this.setState(SequencerState.STOPPED, undefined, {
160
174
  force: true
@@ -176,21 +190,28 @@ export { SequencerState };
176
190
  * - Collect attestations for the block
177
191
  * - Submit block
178
192
  * - If our block for some reason is not included, revert the state
179
- */ async doRealWork() {
193
+ */ async work() {
180
194
  this.setState(SequencerState.SYNCHRONIZING, undefined);
181
- // Check all components are synced to latest as seen by the archiver
182
- const syncedTo = await this.getChainTip();
183
- // Do not go forward with new block if the previous one has not been mined and processed
195
+ const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
196
+ // Check we have not already published a block for this slot (cheapest check)
197
+ if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
198
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`);
199
+ return;
200
+ }
201
+ // Check all components are synced to latest as seen by the archiver (queries all subsystems)
202
+ const syncedTo = await this.checkSync({
203
+ ts,
204
+ slot
205
+ });
184
206
  if (!syncedTo) {
207
+ await this.tryVoteWhenSyncFails({
208
+ slot,
209
+ ts
210
+ });
185
211
  return;
186
212
  }
187
213
  const chainTipArchive = syncedTo.archive;
188
- const newBlockNumber = syncedTo.blockNumber + 1;
189
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
190
- this.setState(SequencerState.PROPOSER_CHECK, slot);
191
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
192
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
193
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
214
+ const newBlockNumber = BlockNumber(syncedTo.blockNumber + 1);
194
215
  const syncLogData = {
195
216
  now,
196
217
  syncedToL1Ts: syncedTo.l1Timestamp,
@@ -201,73 +222,66 @@ export { SequencerState };
201
222
  newBlockNumber,
202
223
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
203
224
  };
204
- if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
205
- this.log.debug(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`, syncLogData);
206
- return;
207
- }
208
- // Check that the slot is not taken by a block already
209
- if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
210
- this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
211
- ...syncLogData,
212
- block: syncedTo.block.header.toInspect()
213
- });
214
- return;
215
- }
216
- // Or that we haven't published it ourselves
217
- if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
218
- this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`, {
219
- ...syncLogData,
220
- block: this.lastBlockPublished.header.toInspect()
221
- });
225
+ // Check that we are a proposer for the next slot
226
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
227
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
228
+ // If we are not a proposer check if we should invalidate a invalid block, and bail
229
+ if (!canPropose) {
230
+ await this.considerInvalidatingBlock(syncedTo, slot);
222
231
  return;
223
232
  }
224
- // Check that we are a proposer for the next slot
225
- let proposerInNextSlot;
226
- try {
227
- proposerInNextSlot = await this.epochCache.getProposerAttesterAddressInNextSlot();
228
- } catch (e) {
229
- if (e instanceof NoCommitteeError) {
230
- this.log.warn(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`);
233
+ // In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
234
+ if (this.config.fishermanMode) {
235
+ if (this.lastSlotForValidationBlock === slot) {
236
+ this.log.trace(`Already validated block building for slot ${slot} (skipping)`, {
237
+ slot
238
+ });
231
239
  return;
232
240
  }
241
+ this.log.debug(`Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`, {
242
+ slot,
243
+ proposer: proposer?.toString()
244
+ });
245
+ // Mark this slot as being validated
246
+ this.lastSlotForValidationBlock = slot;
233
247
  }
234
- // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
235
- // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
236
- const validatorAddresses = this.validatorClient.getValidatorAddresses();
237
- if (proposerInNextSlot !== undefined && !validatorAddresses.some((addr)=>addr.equals(proposerInNextSlot))) {
238
- this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
239
- us: validatorAddresses,
240
- proposer: proposerInNextSlot,
241
- ...syncLogData
248
+ // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
249
+ if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
250
+ this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
251
+ ...syncLogData,
252
+ block: syncedTo.block.header.toInspect()
242
253
  });
243
- // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
244
- if (!syncedTo.pendingChainValidationStatus.valid) {
245
- // We pass i undefined here to get any available publisher.
246
- const { publisher } = await this.publisherFactory.create(undefined);
247
- await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
248
- }
254
+ this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
249
255
  return;
250
256
  }
251
- // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
252
- // if all the previous checks are good, but we do it just in case.
253
- const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
254
257
  // We now need to get ourselves a publisher.
255
258
  // The returned attestor will be the one we provided if we provided one.
256
259
  // Otherwise it will be a valid attestor for the returned publisher.
257
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposerInNextSlot);
260
+ // In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
261
+ const { attestorAddress, publisher } = await this.publisherFactory.create(this.config.fishermanMode ? undefined : proposer);
258
262
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
259
263
  this.publisher = publisher;
264
+ // In fisherman mode, set the actual proposer's address for simulations
265
+ if (this.config.fishermanMode) {
266
+ if (proposer) {
267
+ publisher.setProposerAddressForSimulation(proposer);
268
+ this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
269
+ }
270
+ }
271
+ // Get proposer credentials
260
272
  const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
261
273
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
262
- this.metrics.setCoinbase(coinbase);
263
274
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
264
275
  const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
265
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposerAddressInNextSlot, invalidateBlock);
276
+ // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
277
+ // if all the previous checks are good, but we do it just in case.
278
+ const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposer ?? EthAddress.ZERO, invalidateBlock);
266
279
  if (canProposeCheck === undefined) {
267
280
  this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
268
281
  this.emit('proposer-rollup-check-failed', {
269
282
  reason: 'Rollup contract check failed'
270
283
  });
284
+ this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
271
285
  return;
272
286
  } else if (canProposeCheck.slot !== slot) {
273
287
  this.log.warn(`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${slot} but got ${canProposeCheck.slot}.`, {
@@ -279,9 +293,10 @@ export { SequencerState };
279
293
  this.emit('proposer-rollup-check-failed', {
280
294
  reason: 'Slot mismatch'
281
295
  });
296
+ this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
282
297
  return;
283
- } else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
284
- this.log.warn(`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.blockNumber}.`, {
298
+ } else if (canProposeCheck.checkpointNumber !== CheckpointNumber.fromBlockNumber(newBlockNumber)) {
299
+ this.log.warn(`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}.`, {
285
300
  ...syncLogData,
286
301
  rollup: canProposeCheck,
287
302
  newBlockNumber,
@@ -290,48 +305,78 @@ export { SequencerState };
290
305
  this.emit('proposer-rollup-check-failed', {
291
306
  reason: 'Block mismatch'
292
307
  });
308
+ this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
293
309
  return;
294
310
  }
295
- this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''), {
296
- ...syncLogData,
297
- validatorAddresses
311
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
312
+ ...syncLogData
298
313
  });
299
314
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
300
- const { timestamp } = newGlobalVariables;
301
- const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
302
- const enqueueGovernanceSignalPromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
303
- this.log.error(`Error enqueuing governance vote`, err, {
304
- blockNumber: newBlockNumber,
305
- slot
306
- });
307
- return false;
308
- }) : Promise.resolve(false);
309
- const enqueueSlashingActionsPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
310
- this.log.error(`Error enqueuing slashing actions`, err, {
311
- blockNumber: newBlockNumber,
312
- slot
313
- });
314
- return false;
315
- }) : Promise.resolve(false);
315
+ // Enqueue governance and slashing votes (returns promises that will be awaited later)
316
+ // In fisherman mode, we simulate slashing but don't actually publish to L1
317
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, newGlobalVariables.timestamp);
318
+ // Enqueues block invalidation
316
319
  if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
317
320
  publisher.enqueueInvalidateBlock(invalidateBlock);
318
321
  }
322
+ // Actual block building
319
323
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
324
+ this.metrics.incOpenSlot(slot, proposer?.toString() ?? 'unknown');
325
+ const block = await this.tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock);
326
+ // Wait until the voting promises have resolved, so all requests are enqueued
327
+ await Promise.all(votesPromises);
328
+ // In fisherman mode, we don't publish to L1
329
+ if (this.config.fishermanMode) {
330
+ // Clear pending requests
331
+ publisher.clearPendingRequests();
332
+ if (block) {
333
+ this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
334
+ blockNumber: newBlockNumber,
335
+ slot: Number(slot),
336
+ archive: block.archive.toString(),
337
+ txCount: block.body.txEffects.length
338
+ });
339
+ this.lastBlockPublished = block;
340
+ this.metrics.recordBlockProposalSuccess();
341
+ } else {
342
+ // Block building failed in fisherman mode
343
+ this.log.warn(`Validation block building FAILED for slot ${slot}`, {
344
+ blockNumber: newBlockNumber,
345
+ slot: Number(slot)
346
+ });
347
+ this.metrics.recordBlockProposalFailed('block_build_failed');
348
+ }
349
+ } else {
350
+ // Normal mode: send the tx to L1
351
+ const l1Response = await publisher.sendRequests();
352
+ const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
353
+ if (proposedBlock) {
354
+ this.lastBlockPublished = block;
355
+ this.emit('block-published', {
356
+ blockNumber: newBlockNumber,
357
+ slot: Number(slot)
358
+ });
359
+ await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
360
+ } else if (block) {
361
+ this.emit('block-publish-failed', l1Response ?? {});
362
+ }
363
+ }
364
+ this.setState(SequencerState.IDLE, undefined);
365
+ }
366
+ /** Tries building a block proposal, and if successful, enqueues it for publishing. */ async tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock) {
320
367
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
321
- proposer: proposerInNextSlot?.toString(),
322
- coinbase,
368
+ proposer,
323
369
  publisher: publisher.getSenderAddress(),
324
- feeRecipient,
325
370
  globalVariables: newGlobalVariables.toInspect(),
326
371
  chainTipArchive,
327
372
  blockNumber: newBlockNumber,
328
373
  slot
329
374
  });
330
- // If I created a "partial" header here that should make our job much easier.
331
- const proposalHeader = ProposedBlockHeader.from({
375
+ const proposalHeader = CheckpointHeader.from({
332
376
  ...newGlobalVariables,
333
377
  timestamp: newGlobalVariables.timestamp,
334
378
  lastArchiveRoot: chainTipArchive,
379
+ blockHeadersHash: Fr.ZERO,
335
380
  contentCommitment: ContentCommitment.empty(),
336
381
  totalManaUsed: Fr.ZERO
337
382
  });
@@ -342,7 +387,7 @@ export { SequencerState };
342
387
  // and also we may need to fetch more if we don't have enough valid txs.
343
388
  const pendingTxs = this.p2pClient.iteratePendingTxs();
344
389
  try {
345
- block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerInNextSlot, invalidateBlock, publisher);
390
+ block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposer, invalidateBlock, publisher);
346
391
  } catch (err) {
347
392
  this.emit('block-build-failed', {
348
393
  reason: err.message
@@ -355,6 +400,7 @@ export { SequencerState };
355
400
  slot
356
401
  });
357
402
  }
403
+ this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
358
404
  }
359
405
  } else {
360
406
  this.log.verbose(`Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`, {
@@ -366,28 +412,13 @@ export { SequencerState };
366
412
  minTxs: this.minTxsPerBlock,
367
413
  availableTxs: pendingTxCount
368
414
  });
415
+ this.metrics.recordBlockProposalFailed('insufficient_txs');
369
416
  }
370
- await Promise.all([
371
- enqueueGovernanceSignalPromise,
372
- enqueueSlashingActionsPromise
373
- ]);
374
- const l1Response = await publisher.sendRequests();
375
- const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
376
- if (proposedBlock) {
377
- this.lastBlockPublished = block;
378
- this.emit('block-published', {
379
- blockNumber: newBlockNumber,
380
- slot: Number(slot)
381
- });
382
- this.metrics.incFilledSlot(publisher.getSenderAddress().toString());
383
- } else if (block) {
384
- this.emit('block-publish-failed', l1Response ?? {});
385
- }
386
- this.setState(SequencerState.IDLE, undefined);
417
+ return block;
387
418
  }
388
- async work() {
419
+ async safeWork() {
389
420
  try {
390
- await this.doRealWork();
421
+ await this.work();
391
422
  } catch (err) {
392
423
  if (err instanceof SequencerTooSlowError) {
393
424
  // Log as warn only if we had to abort halfway through the block proposal
@@ -407,6 +438,10 @@ export { SequencerState };
407
438
  }
408
439
  }
409
440
  setState(proposedState, slotNumber, opts = {}) {
441
+ if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
442
+ this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
443
+ throw new SequencerInterruptedError();
444
+ }
410
445
  if (this.state === SequencerState.STOPPED && !opts.force) {
411
446
  this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
412
447
  return;
@@ -416,7 +451,12 @@ export { SequencerState };
416
451
  secondsIntoSlot = this.getSecondsIntoSlot(slotNumber);
417
452
  this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
418
453
  }
419
- this.log.debug(`Transitioning from ${this.state} to ${proposedState}`, {
454
+ const boringStates = [
455
+ SequencerState.IDLE,
456
+ SequencerState.SYNCHRONIZING
457
+ ];
458
+ const logLevel = boringStates.includes(proposedState) && boringStates.includes(this.state) ? 'trace' : 'debug';
459
+ this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, {
420
460
  slotNumber,
421
461
  secondsIntoSlot
422
462
  });
@@ -447,6 +487,7 @@ export { SequencerState };
447
487
  maxTransactions: this.maxTxsPerBlock,
448
488
  maxBlockSize: this.maxBlockSizeInBytes,
449
489
  maxBlockGas: this.maxBlockGas,
490
+ maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
450
491
  deadline
451
492
  };
452
493
  }
@@ -463,13 +504,13 @@ export { SequencerState };
463
504
  */ async buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerAddress, invalidateBlock, publisher) {
464
505
  await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
465
506
  const blockNumber = newGlobalVariables.blockNumber;
466
- const slot = proposalHeader.slotNumber.toBigInt();
467
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
468
- // this.metrics.recordNewBlock(blockNumber, validTxs.length);
507
+ const checkpointNumber = CheckpointNumber.fromBlockNumber(blockNumber);
508
+ const slot = proposalHeader.slotNumber;
509
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
469
510
  const workTimer = new Timer();
470
511
  this.setState(SequencerState.CREATING_BLOCK, slot);
471
512
  try {
472
- const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
513
+ const blockBuilderOptions = this.getBlockBuilderOptions(slot);
473
514
  const buildBlockRes = await this.blockBuilder.buildBlock(pendingTxs, l1ToL2Messages, newGlobalVariables, blockBuilderOptions);
474
515
  const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer, usedTxs, failedTxs } = buildBlockRes;
475
516
  const blockBuildDuration = workTimer.ms();
@@ -485,7 +526,7 @@ export { SequencerState };
485
526
  }
486
527
  // TODO(@PhilWindle) We should probably periodically check for things like another
487
528
  // block being published before ours instead of just waiting on our block
488
- await publisher.validateBlockHeader(block.header.toPropose(), invalidateBlock);
529
+ await publisher.validateBlockHeader(block.getCheckpointHeader(), invalidateBlock);
489
530
  const blockStats = {
490
531
  eventName: 'l2-block-built',
491
532
  creator: proposerAddress?.toString() ?? publisher.getSenderAddress().toString(),
@@ -502,15 +543,23 @@ export { SequencerState };
502
543
  txHashes,
503
544
  ...blockStats
504
545
  });
505
- this.log.debug('Collecting attestations');
506
- const attestations = await this.collectAttestations(block, usedTxs, proposerAddress);
507
- if (attestations !== undefined) {
508
- this.log.verbose(`Collected ${attestations.length} attestations`, {
546
+ // In fisherman mode, skip attestation collection
547
+ let attestationsAndSigners;
548
+ if (this.config.fishermanMode) {
549
+ this.log.debug('Skipping attestation collection');
550
+ attestationsAndSigners = CommitteeAttestationsAndSigners.empty();
551
+ } else {
552
+ this.log.debug('Collecting attestations');
553
+ attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
554
+ this.log.verbose(`Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`, {
509
555
  blockHash,
510
- blockNumber
556
+ blockNumber,
557
+ slot
511
558
  });
512
559
  }
513
- await this.enqueuePublishL2Block(block, attestations, txHashes, invalidateBlock, publisher);
560
+ // In fisherman mode, skip attestation signing
561
+ const attestationsAndSignersSignature = this.config.fishermanMode || !this.validatorClient ? Signature.empty() : await this.validatorClient.signAttestationsAndSigners(attestationsAndSigners, proposerAddress ?? publisher.getSenderAddress());
562
+ await this.enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher);
514
563
  this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
515
564
  return block;
516
565
  } catch (err) {
@@ -519,69 +568,110 @@ export { SequencerState };
519
568
  }
520
569
  }
521
570
  async collectAttestations(block, txs, proposerAddress) {
522
- const { committee } = await this.epochCache.getCommittee(block.header.getSlot());
571
+ const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
523
572
  // We checked above that the committee is defined, so this should never happen.
524
573
  if (!committee) {
525
574
  throw new Error('No committee when collecting attestations');
526
575
  }
527
576
  if (committee.length === 0) {
528
577
  this.log.verbose(`Attesting committee is empty`);
529
- return undefined;
578
+ return CommitteeAttestationsAndSigners.empty();
530
579
  } else {
531
580
  this.log.debug(`Attesting committee length is ${committee.length}`);
532
581
  }
533
582
  if (!this.validatorClient) {
534
- const msg = 'Missing validator client: Cannot collect attestations';
535
- this.log.error(msg);
536
- throw new Error(msg);
583
+ throw new Error('Missing validator client: Cannot collect attestations');
537
584
  }
538
585
  const numberOfRequiredAttestations = Math.floor(committee.length * 2 / 3) + 1;
539
- const slotNumber = block.header.globalVariables.slotNumber.toBigInt();
586
+ const slotNumber = block.header.globalVariables.slotNumber;
540
587
  this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
541
588
  this.log.debug('Creating block proposal for validators');
542
589
  const blockProposalOptions = {
543
- publishFullTxs: !!this.config.publishTxsWithProposals
590
+ publishFullTxs: !!this.config.publishTxsWithProposals,
591
+ broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
544
592
  };
545
- const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.header.toPropose(), block.archive.root, block.header.state, txs, proposerAddress, blockProposalOptions);
593
+ const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.getCheckpointHeader(), block.archive.root, txs, proposerAddress, blockProposalOptions);
546
594
  if (!proposal) {
547
595
  throw new Error(`Failed to create block proposal`);
548
596
  }
549
597
  if (this.config.skipCollectingAttestations) {
550
598
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
551
599
  const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
552
- return orderAttestations(attestations ?? [], committee);
600
+ return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
553
601
  }
554
602
  this.log.debug('Broadcasting block proposal to validators');
555
603
  await this.validatorClient.broadcastBlockProposal(proposal);
556
604
  const attestationTimeAllowed = this.enforceTimeTable ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_BLOCK) : this.aztecSlotDuration;
557
605
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
558
606
  const timer = new Timer();
559
- let collectedAttestionsCount = 0;
607
+ let collectedAttestationsCount = 0;
560
608
  try {
561
609
  const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
562
610
  const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations, attestationDeadline);
563
- collectedAttestionsCount = attestations.length;
611
+ collectedAttestationsCount = attestations.length;
564
612
  // note: the smart contract requires that the signatures are provided in the order of the committee
565
- return orderAttestations(attestations, committee);
613
+ const sorted = orderAttestations(attestations, committee);
614
+ // manipulate the attestations if we've been configured to do so
615
+ if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
616
+ return this.manipulateAttestations(block, epoch, seed, committee, sorted);
617
+ }
618
+ return new CommitteeAttestationsAndSigners(sorted);
566
619
  } catch (err) {
567
620
  if (err && err instanceof AttestationTimeoutError) {
568
- collectedAttestionsCount = err.collectedCount;
621
+ collectedAttestationsCount = err.collectedCount;
569
622
  }
570
623
  throw err;
571
624
  } finally{
572
- this.metrics.recordCollectedAttestations(collectedAttestionsCount, timer.ms());
625
+ this.metrics.recordCollectedAttestations(collectedAttestationsCount, timer.ms());
573
626
  }
574
627
  }
628
+ /** Breaks the attestations before publishing based on attack configs */ manipulateAttestations(block, epoch, seed, committee, attestations) {
629
+ // Compute the proposer index in the committee, since we dont want to tweak it.
630
+ // Otherwise, the L1 rollup contract will reject the block outright.
631
+ const proposerIndex = Number(this.epochCache.computeProposerIndex(block.slot, epoch, seed, BigInt(committee.length)));
632
+ if (this.config.injectFakeAttestation) {
633
+ // Find non-empty attestations that are not from the proposer
634
+ const nonProposerIndices = [];
635
+ for(let i = 0; i < attestations.length; i++){
636
+ if (!attestations[i].signature.isEmpty() && i !== proposerIndex) {
637
+ nonProposerIndices.push(i);
638
+ }
639
+ }
640
+ if (nonProposerIndices.length > 0) {
641
+ const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
642
+ this.log.warn(`Injecting fake attestation in block ${block.number} at index ${targetIndex}`);
643
+ unfreeze(attestations[targetIndex]).signature = Signature.random();
644
+ }
645
+ return new CommitteeAttestationsAndSigners(attestations);
646
+ }
647
+ if (this.config.shuffleAttestationOrdering) {
648
+ this.log.warn(`Shuffling attestation ordering in block ${block.number} (proposer index ${proposerIndex})`);
649
+ const shuffled = [
650
+ ...attestations
651
+ ];
652
+ const [i, j] = [
653
+ (proposerIndex + 1) % shuffled.length,
654
+ (proposerIndex + 2) % shuffled.length
655
+ ];
656
+ const valueI = shuffled[i];
657
+ const valueJ = shuffled[j];
658
+ shuffled[i] = valueJ;
659
+ shuffled[j] = valueI;
660
+ const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
661
+ return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
662
+ }
663
+ return new CommitteeAttestationsAndSigners(attestations);
664
+ }
575
665
  /**
576
666
  * Publishes the L2Block to the rollup contract.
577
667
  * @param block - The L2Block to be published.
578
- */ async enqueuePublishL2Block(block, attestations, txHashes, invalidateBlock, publisher) {
668
+ */ async enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher) {
579
669
  // Publishes new block to the network and awaits the tx to be mined
580
- this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
670
+ this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
581
671
  // Time out tx at the end of the slot
582
- const slot = block.header.globalVariables.slotNumber.toNumber();
672
+ const slot = block.header.globalVariables.slotNumber;
583
673
  const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
584
- const enqueued = await publisher.enqueueProposeL2Block(block, attestations, txHashes, {
674
+ const enqueued = await publisher.enqueueProposeL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, {
585
675
  txTimeoutAt,
586
676
  forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber
587
677
  });
@@ -592,8 +682,20 @@ export { SequencerState };
592
682
  /**
593
683
  * Returns whether all dependencies have caught up.
594
684
  * We don't check against the previous block submitted since it may have been reorg'd out.
595
- * @returns Boolean indicating if our dependencies are synced to the latest block.
596
- */ async getChainTip() {
685
+ */ async checkSync(args) {
686
+ // Check that the archiver and dependencies have synced to the previous L1 slot at least
687
+ // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
688
+ // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
689
+ const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
690
+ const { slot, ts } = args;
691
+ if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
692
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
693
+ slot,
694
+ ts,
695
+ l1Timestamp
696
+ });
697
+ return undefined;
698
+ }
597
699
  const syncedBlocks = await Promise.all([
598
700
  this.worldState.status().then(({ syncSummary })=>({
599
701
  number: syncSummary.latestBlockNumber,
@@ -602,66 +704,218 @@ export { SequencerState };
602
704
  this.l2BlockSource.getL2Tips().then((t)=>t.latest),
603
705
  this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
604
706
  this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
605
- this.l2BlockSource.getL1Timestamp(),
606
707
  this.l2BlockSource.getPendingChainValidationStatus()
607
708
  ]);
608
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] = syncedBlocks;
609
- // The archiver reports 'undefined' hash for the genesis block
610
- // because it doesn't have access to world state to compute it (facepalm)
611
- const result = l2BlockSource.hash === undefined ? worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 : worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
612
- const logData = {
613
- worldState,
614
- l2BlockSource,
615
- p2p,
616
- l1ToL2MessageSource
617
- };
618
- this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
709
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
710
+ // 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,
711
+ // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
712
+ const result = l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 || worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
619
713
  if (!result) {
714
+ this.log.debug(`Sequencer sync check failed`, {
715
+ worldState,
716
+ l2BlockSource,
717
+ p2p,
718
+ l1ToL2MessageSource
719
+ });
620
720
  return undefined;
621
721
  }
722
+ // Special case for genesis state
622
723
  const blockNumber = worldState.number;
623
- if (blockNumber >= INITIAL_L2_BLOCK_NUM) {
624
- const block = await this.l2BlockSource.getBlock(blockNumber);
625
- if (!block) {
626
- // this shouldn't really happen because a moment ago we checked that all components were in sync
627
- this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
628
- return undefined;
629
- }
630
- return {
631
- block,
632
- blockNumber: block.number,
633
- archive: block.archive.root,
634
- l1Timestamp,
635
- pendingChainValidationStatus
636
- };
637
- } else {
724
+ if (blockNumber < INITIAL_L2_BLOCK_NUM) {
638
725
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
639
726
  return {
640
- blockNumber: INITIAL_L2_BLOCK_NUM - 1,
727
+ blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1),
641
728
  archive,
642
729
  l1Timestamp,
643
730
  pendingChainValidationStatus
644
731
  };
645
732
  }
733
+ const block = await this.l2BlockSource.getBlock(blockNumber);
734
+ if (!block) {
735
+ // this shouldn't really happen because a moment ago we checked that all components were in sync
736
+ this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
737
+ return undefined;
738
+ }
739
+ return {
740
+ block,
741
+ blockNumber: block.number,
742
+ archive: block.archive.root,
743
+ l1Timestamp,
744
+ pendingChainValidationStatus
745
+ };
746
+ }
747
+ /**
748
+ * Enqueues governance and slashing votes with the publisher. Does not block.
749
+ * @param publisher - The publisher to enqueue votes with
750
+ * @param attestorAddress - The attestor address to use for signing
751
+ * @param slot - The slot number
752
+ * @param timestamp - The timestamp for the votes
753
+ * @param context - Optional context for logging (e.g., block number)
754
+ * @returns A tuple of [governanceEnqueued, slashingEnqueued]
755
+ */ enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, timestamp) {
756
+ try {
757
+ const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
758
+ const enqueueGovernancePromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
759
+ this.log.error(`Error enqueuing governance vote`, err, {
760
+ slot
761
+ });
762
+ return false;
763
+ }) : undefined;
764
+ const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>{
765
+ // Record metrics for fisherman mode
766
+ if (this.config.fishermanMode && actions.length > 0) {
767
+ this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
768
+ slot,
769
+ actionCount: actions.length
770
+ });
771
+ this.metrics.recordSlashingAttempt(actions.length);
772
+ }
773
+ // Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
774
+ return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
775
+ }).catch((err)=>{
776
+ this.log.error(`Error enqueuing slashing actions`, err, {
777
+ slot
778
+ });
779
+ return false;
780
+ }) : undefined;
781
+ return [
782
+ enqueueGovernancePromise,
783
+ enqueueSlashingPromise
784
+ ];
785
+ } catch (err) {
786
+ this.log.error(`Error enqueueing governance and slashing votes`, err);
787
+ return [
788
+ undefined,
789
+ undefined
790
+ ];
791
+ }
792
+ }
793
+ /**
794
+ * Checks if we are the proposer for the next slot.
795
+ * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
796
+ */ async checkCanPropose(slot) {
797
+ let proposer;
798
+ try {
799
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
800
+ } catch (e) {
801
+ if (e instanceof NoCommitteeError) {
802
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
803
+ return [
804
+ false,
805
+ undefined
806
+ ];
807
+ }
808
+ this.log.error(`Error getting proposer for slot ${slot}`, e);
809
+ return [
810
+ false,
811
+ undefined
812
+ ];
813
+ }
814
+ // If proposer is undefined, then the committee is empty and anyone may propose
815
+ if (proposer === undefined) {
816
+ return [
817
+ true,
818
+ undefined
819
+ ];
820
+ }
821
+ // In fisherman mode, just return the current proposer
822
+ if (this.config.fishermanMode) {
823
+ return [
824
+ true,
825
+ proposer
826
+ ];
827
+ }
828
+ const validatorAddresses = this.validatorClient.getValidatorAddresses();
829
+ const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
830
+ if (!weAreProposer) {
831
+ this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, {
832
+ validatorAddresses,
833
+ proposer
834
+ });
835
+ return [
836
+ false,
837
+ proposer
838
+ ];
839
+ }
840
+ return [
841
+ true,
842
+ proposer
843
+ ];
844
+ }
845
+ /**
846
+ * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
847
+ * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
848
+ */ async tryVoteWhenSyncFails(args) {
849
+ const { slot, ts } = args;
850
+ // Prevent duplicate attempts in the same slot
851
+ if (this.lastSlotForVoteWhenSyncFailed === slot) {
852
+ this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
853
+ return;
854
+ }
855
+ // Check if we're past the max time for initializing a proposal
856
+ const secondsIntoSlot = this.getSecondsIntoSlot(slot);
857
+ const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
858
+ // If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
859
+ // We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
860
+ if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
861
+ this.log.trace(`Not attempting to vote since there is still for block building`, {
862
+ secondsIntoSlot,
863
+ maxAllowedTime
864
+ });
865
+ return;
866
+ }
867
+ this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
868
+ secondsIntoSlot,
869
+ maxAllowedTime
870
+ });
871
+ // Check if we're a proposer or proposal is open
872
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
873
+ if (!canPropose) {
874
+ this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, {
875
+ slot,
876
+ proposer
877
+ });
878
+ return;
879
+ }
880
+ // Mark this slot as attempted
881
+ this.lastSlotForVoteWhenSyncFailed = slot;
882
+ // Get a publisher for voting
883
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
884
+ this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
885
+ attestorAddress,
886
+ slot
887
+ });
888
+ // Enqueue governance and slashing votes using the shared helper method
889
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
890
+ await Promise.all(votesPromises);
891
+ if (votesPromises.every((p)=>!p)) {
892
+ this.log.debug(`No votes to enqueue for slot ${slot}`);
893
+ return;
894
+ }
895
+ this.log.info(`Voting in slot ${slot} despite sync failure`, {
896
+ slot
897
+ });
898
+ await publisher.sendRequests();
646
899
  }
647
900
  /**
648
901
  * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
649
902
  * has been there without being invalidated and whether the sequencer is in the committee or not. We always
650
903
  * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
651
904
  * and if they fail, any sequencer will try as well.
652
- */ async considerInvalidatingBlock(syncedTo, currentSlot, ourValidatorAddresses, publisher) {
905
+ */ async considerInvalidatingBlock(syncedTo, currentSlot) {
653
906
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
654
907
  if (pendingChainValidationStatus.valid) {
655
908
  return;
656
909
  }
657
- const invalidL1Timestamp = pendingChainValidationStatus.block.l1.timestamp;
658
- const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidL1Timestamp);
659
- const invalidBlockNumber = pendingChainValidationStatus.block.block.number;
910
+ const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
911
+ const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
912
+ const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
913
+ const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
660
914
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
661
915
  const logData = {
662
- invalidL1Timestamp,
916
+ invalidL1Timestamp: invalidBlockTimestamp,
663
917
  l1Timestamp,
664
- invalidBlock: pendingChainValidationStatus.block.block.toBlockInfo(),
918
+ invalidBlock: pendingChainValidationStatus.block,
665
919
  secondsBeforeInvalidatingBlockAsCommitteeMember,
666
920
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
667
921
  ourValidatorAddresses,
@@ -674,6 +928,21 @@ export { SequencerState };
674
928
  this.log.debug(`Not invalidating pending chain`, logData);
675
929
  return;
676
930
  }
931
+ let validatorToUse;
932
+ if (invalidateAsCommitteeMember) {
933
+ // When invalidating as a committee member, use first validator that's actually in the committee
934
+ const { committee } = await this.epochCache.getCommittee(currentSlot);
935
+ if (committee) {
936
+ const committeeSet = new Set(committee.map((addr)=>addr.toString()));
937
+ validatorToUse = ourValidatorAddresses.find((addr)=>committeeSet.has(addr.toString())) ?? ourValidatorAddresses[0];
938
+ } else {
939
+ validatorToUse = ourValidatorAddresses[0];
940
+ }
941
+ } else {
942
+ // When invalidating as a non-committee member, use the first validator
943
+ validatorToUse = ourValidatorAddresses[0];
944
+ }
945
+ const { publisher } = await this.publisherFactory.create(validatorToUse);
677
946
  const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
678
947
  if (!invalidateBlock) {
679
948
  this.log.warn(`Failed to simulate invalidate block`, logData);
@@ -681,10 +950,15 @@ export { SequencerState };
681
950
  }
682
951
  this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
683
952
  publisher.enqueueInvalidateBlock(invalidateBlock);
684
- await publisher.sendRequests();
953
+ if (!this.config.fishermanMode) {
954
+ await publisher.sendRequests();
955
+ } else {
956
+ this.log.info('Invalidating block in fisherman mode, clearing pending requests');
957
+ publisher.clearPendingRequests();
958
+ }
685
959
  }
686
960
  getSlotStartBuildTimestamp(slotNumber) {
687
- return Number(this.l1Constants.l1GenesisTime) + Number(slotNumber) * this.l1Constants.slotDuration - this.l1Constants.ethereumSlotDuration;
961
+ return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
688
962
  }
689
963
  getSecondsIntoSlot(slotNumber) {
690
964
  const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
@@ -702,7 +976,7 @@ export { SequencerState };
702
976
  }
703
977
  _ts_decorate([
704
978
  trackSpan('Sequencer.work')
705
- ], Sequencer.prototype, "work", null);
979
+ ], Sequencer.prototype, "safeWork", null);
706
980
  _ts_decorate([
707
981
  trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables)=>({
708
982
  [Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber