@aztec/sequencer-client 3.0.0-devnet.5 → 3.0.0-devnet.6-patch.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dest/client/index.d.ts +1 -1
  2. package/dest/client/sequencer-client.d.ts +2 -2
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +6 -2
  5. package/dest/config.d.ts +3 -2
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +11 -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 +7 -2
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +12 -1
  16. package/dest/publisher/index.d.ts +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -2
  18. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  19. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  20. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher.d.ts +40 -31
  22. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher.js +117 -62
  24. package/dest/sequencer/block_builder.d.ts +4 -3
  25. package/dest/sequencer/block_builder.d.ts.map +1 -1
  26. package/dest/sequencer/block_builder.js +5 -8
  27. package/dest/sequencer/config.d.ts +2 -2
  28. package/dest/sequencer/config.d.ts.map +1 -1
  29. package/dest/sequencer/errors.d.ts +1 -1
  30. package/dest/sequencer/errors.d.ts.map +1 -1
  31. package/dest/sequencer/index.d.ts +1 -1
  32. package/dest/sequencer/metrics.d.ts +12 -3
  33. package/dest/sequencer/metrics.d.ts.map +1 -1
  34. package/dest/sequencer/metrics.js +38 -0
  35. package/dest/sequencer/sequencer.d.ts +48 -27
  36. package/dest/sequencer/sequencer.d.ts.map +1 -1
  37. package/dest/sequencer/sequencer.js +418 -166
  38. package/dest/sequencer/timetable.d.ts +3 -1
  39. package/dest/sequencer/timetable.d.ts.map +1 -1
  40. package/dest/sequencer/utils.d.ts +1 -1
  41. package/dest/test/index.d.ts +2 -2
  42. package/dest/test/index.d.ts.map +1 -1
  43. package/dest/tx_validator/nullifier_cache.d.ts +1 -1
  44. package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
  45. package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
  46. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  47. package/dest/tx_validator/tx_validator_factory.js +1 -1
  48. package/package.json +31 -30
  49. package/src/client/sequencer-client.ts +6 -9
  50. package/src/config.ts +12 -6
  51. package/src/global_variable_builder/global_builder.ts +19 -17
  52. package/src/publisher/config.ts +17 -6
  53. package/src/publisher/sequencer-publisher-factory.ts +4 -2
  54. package/src/publisher/sequencer-publisher.ts +165 -94
  55. package/src/sequencer/block_builder.ts +8 -12
  56. package/src/sequencer/config.ts +1 -1
  57. package/src/sequencer/metrics.ts +52 -3
  58. package/src/sequencer/sequencer.ts +480 -198
  59. package/src/sequencer/timetable.ts +7 -0
  60. package/src/test/index.ts +1 -1
  61. package/src/tx_validator/tx_validator_factory.ts +3 -2
@@ -4,18 +4,21 @@ 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 { BLOBS_PER_BLOCK, FIELDS_PER_BLOB, 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';
10
- import { randomInt } from '@aztec/foundation/crypto';
13
+ import { randomInt } from '@aztec/foundation/crypto/random';
14
+ import { Fr } from '@aztec/foundation/curves/bn254';
11
15
  import { EthAddress } from '@aztec/foundation/eth-address';
12
16
  import { Signature } from '@aztec/foundation/eth-signature';
13
- import { Fr } from '@aztec/foundation/fields';
14
17
  import { createLogger } from '@aztec/foundation/log';
15
18
  import { RunningPromise } from '@aztec/foundation/running-promise';
16
19
  import { Timer } from '@aztec/foundation/timer';
17
20
  import { unfreeze } from '@aztec/foundation/types';
18
- import { CommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
21
+ import { CommitteeAttestationsAndSigners, MaliciousCommitteeAttestationsAndSigners } from '@aztec/stdlib/block';
19
22
  import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
20
23
  import { Gas } from '@aztec/stdlib/gas';
21
24
  import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
@@ -68,6 +71,8 @@ export { SequencerState };
68
71
  metrics;
69
72
  lastBlockPublished;
70
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;
71
76
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
72
77
  enforceTimeTable;
73
78
  // This shouldn't be here as this gets re-created each time we build/propose a block.
@@ -78,6 +83,10 @@ export { SequencerState };
78
83
  publisher;
79
84
  constructor(publisherFactory, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, epochCache, rollupContract, config, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
80
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;
86
+ // Add [FISHERMAN] prefix to logger if in fisherman mode
87
+ if (this.config.fishermanMode) {
88
+ this.log = log.createChild('[FISHERMAN]');
89
+ }
81
90
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
82
91
  // Initialize config
83
92
  this.updateConfig(this.config);
@@ -138,12 +147,14 @@ export { SequencerState };
138
147
  }, this.metrics, this.log);
139
148
  }
140
149
  async init() {
150
+ // Takes ~3s to precompute some tables.
151
+ getKzg();
141
152
  this.publisher = (await this.publisherFactory.create(undefined)).publisher;
142
153
  }
143
154
  /**
144
155
  * Starts the sequencer and moves to IDLE state.
145
156
  */ start() {
146
- 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);
147
158
  this.setState(SequencerState.IDLE, undefined, {
148
159
  force: true
149
160
  });
@@ -179,21 +190,28 @@ export { SequencerState };
179
190
  * - Collect attestations for the block
180
191
  * - Submit block
181
192
  * - If our block for some reason is not included, revert the state
182
- */ async doRealWork() {
193
+ */ async work() {
183
194
  this.setState(SequencerState.SYNCHRONIZING, undefined);
184
- // Check all components are synced to latest as seen by the archiver
185
- const syncedTo = await this.getChainTip();
186
- // 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
+ });
187
206
  if (!syncedTo) {
207
+ await this.tryVoteWhenSyncFails({
208
+ slot,
209
+ ts
210
+ });
188
211
  return;
189
212
  }
190
213
  const chainTipArchive = syncedTo.archive;
191
- const newBlockNumber = syncedTo.blockNumber + 1;
192
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
193
- this.setState(SequencerState.PROPOSER_CHECK, slot);
194
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
195
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
196
- // 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);
197
215
  const syncLogData = {
198
216
  now,
199
217
  syncedToL1Ts: syncedTo.l1Timestamp,
@@ -204,72 +222,66 @@ export { SequencerState };
204
222
  newBlockNumber,
205
223
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
206
224
  };
207
- if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
208
- this.log.debug(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`, syncLogData);
209
- return;
210
- }
211
- // Check that the slot is not taken by a block already
212
- if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
213
- this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
214
- ...syncLogData,
215
- block: syncedTo.block.header.toInspect()
216
- });
217
- return;
218
- }
219
- // Or that we haven't published it ourselves
220
- if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
221
- this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`, {
222
- ...syncLogData,
223
- block: this.lastBlockPublished.header.toInspect()
224
- });
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);
225
231
  return;
226
232
  }
227
- // Check that we are a proposer for the next slot
228
- let proposerInNextSlot;
229
- try {
230
- proposerInNextSlot = await this.epochCache.getProposerAttesterAddressInNextSlot();
231
- } catch (e) {
232
- if (e instanceof NoCommitteeError) {
233
- 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
+ });
234
239
  return;
235
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;
236
247
  }
237
- // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
238
- // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
239
- const validatorAddresses = this.validatorClient.getValidatorAddresses();
240
- if (proposerInNextSlot !== undefined && !validatorAddresses.some((addr)=>addr.equals(proposerInNextSlot))) {
241
- this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
242
- us: validatorAddresses,
243
- proposer: proposerInNextSlot,
244
- ...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()
245
253
  });
246
- // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
247
- if (!syncedTo.pendingChainValidationStatus.valid) {
248
- // We pass i undefined here to get any available publisher.
249
- const { publisher } = await this.publisherFactory.create(undefined);
250
- await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
251
- }
254
+ this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
252
255
  return;
253
256
  }
254
- // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
255
- // if all the previous checks are good, but we do it just in case.
256
- const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
257
257
  // We now need to get ourselves a publisher.
258
258
  // The returned attestor will be the one we provided if we provided one.
259
259
  // Otherwise it will be a valid attestor for the returned publisher.
260
- 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);
261
262
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
262
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
263
272
  const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
264
273
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
265
274
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
266
275
  const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
267
- 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);
268
279
  if (canProposeCheck === undefined) {
269
280
  this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
270
281
  this.emit('proposer-rollup-check-failed', {
271
282
  reason: 'Rollup contract check failed'
272
283
  });
284
+ this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
273
285
  return;
274
286
  } else if (canProposeCheck.slot !== slot) {
275
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}.`, {
@@ -281,9 +293,10 @@ export { SequencerState };
281
293
  this.emit('proposer-rollup-check-failed', {
282
294
  reason: 'Slot mismatch'
283
295
  });
296
+ this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
284
297
  return;
285
- } else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
286
- 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}.`, {
287
300
  ...syncLogData,
288
301
  rollup: canProposeCheck,
289
302
  newBlockNumber,
@@ -292,49 +305,78 @@ export { SequencerState };
292
305
  this.emit('proposer-rollup-check-failed', {
293
306
  reason: 'Block mismatch'
294
307
  });
308
+ this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
295
309
  return;
296
310
  }
297
- this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''), {
298
- ...syncLogData,
299
- validatorAddresses
311
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
312
+ ...syncLogData
300
313
  });
301
314
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
302
- const { timestamp } = newGlobalVariables;
303
- const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
304
- const enqueueGovernanceSignalPromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
305
- this.log.error(`Error enqueuing governance vote`, err, {
306
- blockNumber: newBlockNumber,
307
- slot
308
- });
309
- return false;
310
- }) : Promise.resolve(false);
311
- const enqueueSlashingActionsPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
312
- this.log.error(`Error enqueuing slashing actions`, err, {
313
- blockNumber: newBlockNumber,
314
- slot
315
- });
316
- return false;
317
- }) : 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
318
319
  if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
319
320
  publisher.enqueueInvalidateBlock(invalidateBlock);
320
321
  }
322
+ // Actual block building
321
323
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
322
- this.metrics.incOpenSlot(slot, proposerAddressInNextSlot.toString());
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) {
323
367
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
324
- proposer: proposerInNextSlot?.toString(),
325
- coinbase,
368
+ proposer,
326
369
  publisher: publisher.getSenderAddress(),
327
- feeRecipient,
328
370
  globalVariables: newGlobalVariables.toInspect(),
329
371
  chainTipArchive,
330
372
  blockNumber: newBlockNumber,
331
373
  slot
332
374
  });
333
- // If I created a "partial" header here that should make our job much easier.
334
375
  const proposalHeader = CheckpointHeader.from({
335
376
  ...newGlobalVariables,
336
377
  timestamp: newGlobalVariables.timestamp,
337
378
  lastArchiveRoot: chainTipArchive,
379
+ blockHeadersHash: Fr.ZERO,
338
380
  contentCommitment: ContentCommitment.empty(),
339
381
  totalManaUsed: Fr.ZERO
340
382
  });
@@ -345,7 +387,7 @@ export { SequencerState };
345
387
  // and also we may need to fetch more if we don't have enough valid txs.
346
388
  const pendingTxs = this.p2pClient.iteratePendingTxs();
347
389
  try {
348
- block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerInNextSlot, invalidateBlock, publisher);
390
+ block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposer, invalidateBlock, publisher);
349
391
  } catch (err) {
350
392
  this.emit('block-build-failed', {
351
393
  reason: err.message
@@ -358,6 +400,7 @@ export { SequencerState };
358
400
  slot
359
401
  });
360
402
  }
403
+ this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
361
404
  }
362
405
  } else {
363
406
  this.log.verbose(`Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`, {
@@ -369,28 +412,13 @@ export { SequencerState };
369
412
  minTxs: this.minTxsPerBlock,
370
413
  availableTxs: pendingTxCount
371
414
  });
415
+ this.metrics.recordBlockProposalFailed('insufficient_txs');
372
416
  }
373
- await Promise.all([
374
- enqueueGovernanceSignalPromise,
375
- enqueueSlashingActionsPromise
376
- ]);
377
- const l1Response = await publisher.sendRequests();
378
- const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
379
- if (proposedBlock) {
380
- this.lastBlockPublished = block;
381
- this.emit('block-published', {
382
- blockNumber: newBlockNumber,
383
- slot: Number(slot)
384
- });
385
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
386
- } else if (block) {
387
- this.emit('block-publish-failed', l1Response ?? {});
388
- }
389
- this.setState(SequencerState.IDLE, undefined);
417
+ return block;
390
418
  }
391
- async work() {
419
+ async safeWork() {
392
420
  try {
393
- await this.doRealWork();
421
+ await this.work();
394
422
  } catch (err) {
395
423
  if (err instanceof SequencerTooSlowError) {
396
424
  // Log as warn only if we had to abort halfway through the block proposal
@@ -459,7 +487,7 @@ export { SequencerState };
459
487
  maxTransactions: this.maxTxsPerBlock,
460
488
  maxBlockSize: this.maxBlockSizeInBytes,
461
489
  maxBlockGas: this.maxBlockGas,
462
- maxBlobFields: BLOBS_PER_BLOCK * FIELDS_PER_BLOB,
490
+ maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
463
491
  deadline
464
492
  };
465
493
  }
@@ -476,12 +504,13 @@ export { SequencerState };
476
504
  */ async buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerAddress, invalidateBlock, publisher) {
477
505
  await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
478
506
  const blockNumber = newGlobalVariables.blockNumber;
479
- const slot = proposalHeader.slotNumber.toBigInt();
480
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
507
+ const checkpointNumber = CheckpointNumber.fromBlockNumber(blockNumber);
508
+ const slot = proposalHeader.slotNumber;
509
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
481
510
  const workTimer = new Timer();
482
511
  this.setState(SequencerState.CREATING_BLOCK, slot);
483
512
  try {
484
- const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
513
+ const blockBuilderOptions = this.getBlockBuilderOptions(slot);
485
514
  const buildBlockRes = await this.blockBuilder.buildBlock(pendingTxs, l1ToL2Messages, newGlobalVariables, blockBuilderOptions);
486
515
  const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer, usedTxs, failedTxs } = buildBlockRes;
487
516
  const blockBuildDuration = workTimer.ms();
@@ -514,16 +543,22 @@ export { SequencerState };
514
543
  txHashes,
515
544
  ...blockStats
516
545
  });
517
- this.log.debug('Collecting attestations');
518
- const attestations = await this.collectAttestations(block, usedTxs, proposerAddress);
519
- if (attestations !== undefined) {
520
- 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}`, {
521
555
  blockHash,
522
- blockNumber
556
+ blockNumber,
557
+ slot
523
558
  });
524
559
  }
525
- const attestationsAndSigners = new CommitteeAttestationsAndSigners(attestations ?? []);
526
- const attestationsAndSignersSignature = this.validatorClient ? await this.validatorClient.signAttestationsAndSigners(attestationsAndSigners, proposerAddress ?? publisher.getSenderAddress()) : Signature.empty();
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());
527
562
  await this.enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher);
528
563
  this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
529
564
  return block;
@@ -533,38 +568,36 @@ export { SequencerState };
533
568
  }
534
569
  }
535
570
  async collectAttestations(block, txs, proposerAddress) {
536
- const { committee } = await this.epochCache.getCommittee(block.header.getSlot());
571
+ const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
537
572
  // We checked above that the committee is defined, so this should never happen.
538
573
  if (!committee) {
539
574
  throw new Error('No committee when collecting attestations');
540
575
  }
541
576
  if (committee.length === 0) {
542
577
  this.log.verbose(`Attesting committee is empty`);
543
- return undefined;
578
+ return CommitteeAttestationsAndSigners.empty();
544
579
  } else {
545
580
  this.log.debug(`Attesting committee length is ${committee.length}`);
546
581
  }
547
582
  if (!this.validatorClient) {
548
- const msg = 'Missing validator client: Cannot collect attestations';
549
- this.log.error(msg);
550
- throw new Error(msg);
583
+ throw new Error('Missing validator client: Cannot collect attestations');
551
584
  }
552
585
  const numberOfRequiredAttestations = Math.floor(committee.length * 2 / 3) + 1;
553
- const slotNumber = block.header.globalVariables.slotNumber.toBigInt();
586
+ const slotNumber = block.header.globalVariables.slotNumber;
554
587
  this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
555
588
  this.log.debug('Creating block proposal for validators');
556
589
  const blockProposalOptions = {
557
590
  publishFullTxs: !!this.config.publishTxsWithProposals,
558
591
  broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
559
592
  };
560
- const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.getCheckpointHeader(), 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);
561
594
  if (!proposal) {
562
595
  throw new Error(`Failed to create block proposal`);
563
596
  }
564
597
  if (this.config.skipCollectingAttestations) {
565
598
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
566
599
  const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
567
- return orderAttestations(attestations ?? [], committee);
600
+ return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
568
601
  }
569
602
  this.log.debug('Broadcasting block proposal to validators');
570
603
  await this.validatorClient.broadcastBlockProposal(proposal);
@@ -578,13 +611,11 @@ export { SequencerState };
578
611
  collectedAttestationsCount = attestations.length;
579
612
  // note: the smart contract requires that the signatures are provided in the order of the committee
580
613
  const sorted = orderAttestations(attestations, committee);
581
- if (this.config.injectFakeAttestation) {
582
- const nonEmpty = sorted.filter((a)=>!a.signature.isEmpty());
583
- const randomIndex = randomInt(nonEmpty.length);
584
- this.log.warn(`Injecting fake attestation in block ${block.number}`);
585
- unfreeze(nonEmpty[randomIndex]).signature = Signature.random();
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);
586
617
  }
587
- return sorted;
618
+ return new CommitteeAttestationsAndSigners(sorted);
588
619
  } catch (err) {
589
620
  if (err && err instanceof AttestationTimeoutError) {
590
621
  collectedAttestationsCount = err.collectedCount;
@@ -594,14 +625,51 @@ export { SequencerState };
594
625
  this.metrics.recordCollectedAttestations(collectedAttestationsCount, timer.ms());
595
626
  }
596
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
+ }
597
665
  /**
598
666
  * Publishes the L2Block to the rollup contract.
599
667
  * @param block - The L2Block to be published.
600
668
  */ async enqueuePublishL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, invalidateBlock, publisher) {
601
669
  // Publishes new block to the network and awaits the tx to be mined
602
- this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
670
+ this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
603
671
  // Time out tx at the end of the slot
604
- const slot = block.header.globalVariables.slotNumber.toNumber();
672
+ const slot = block.header.globalVariables.slotNumber;
605
673
  const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
606
674
  const enqueued = await publisher.enqueueProposeL2Block(block, attestationsAndSigners, attestationsAndSignersSignature, {
607
675
  txTimeoutAt,
@@ -614,8 +682,20 @@ export { SequencerState };
614
682
  /**
615
683
  * Returns whether all dependencies have caught up.
616
684
  * We don't check against the previous block submitted since it may have been reorg'd out.
617
- * @returns Boolean indicating if our dependencies are synced to the latest block.
618
- */ 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
+ }
619
699
  const syncedBlocks = await Promise.all([
620
700
  this.worldState.status().then(({ syncSummary })=>({
621
701
  number: syncSummary.latestBlockNumber,
@@ -624,54 +704,205 @@ export { SequencerState };
624
704
  this.l2BlockSource.getL2Tips().then((t)=>t.latest),
625
705
  this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
626
706
  this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
627
- this.l2BlockSource.getL1Timestamp(),
628
707
  this.l2BlockSource.getPendingChainValidationStatus()
629
708
  ]);
630
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] = syncedBlocks;
631
- // The archiver reports 'undefined' hash for the genesis block
632
- // because it doesn't have access to world state to compute it (facepalm)
633
- 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;
634
- const logData = {
635
- worldState,
636
- l2BlockSource,
637
- p2p,
638
- l1ToL2MessageSource
639
- };
640
- 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;
641
713
  if (!result) {
714
+ this.log.debug(`Sequencer sync check failed`, {
715
+ worldState,
716
+ l2BlockSource,
717
+ p2p,
718
+ l1ToL2MessageSource
719
+ });
642
720
  return undefined;
643
721
  }
722
+ // Special case for genesis state
644
723
  const blockNumber = worldState.number;
645
- if (blockNumber >= INITIAL_L2_BLOCK_NUM) {
646
- const block = await this.l2BlockSource.getBlock(blockNumber);
647
- if (!block) {
648
- // this shouldn't really happen because a moment ago we checked that all components were in sync
649
- this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
650
- return undefined;
651
- }
652
- return {
653
- block,
654
- blockNumber: block.number,
655
- archive: block.archive.root,
656
- l1Timestamp,
657
- pendingChainValidationStatus
658
- };
659
- } else {
724
+ if (blockNumber < INITIAL_L2_BLOCK_NUM) {
660
725
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
661
726
  return {
662
- blockNumber: INITIAL_L2_BLOCK_NUM - 1,
727
+ blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1),
663
728
  archive,
664
729
  l1Timestamp,
665
730
  pendingChainValidationStatus
666
731
  };
667
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();
668
899
  }
669
900
  /**
670
901
  * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
671
902
  * has been there without being invalidated and whether the sequencer is in the committee or not. We always
672
903
  * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
673
904
  * and if they fail, any sequencer will try as well.
674
- */ async considerInvalidatingBlock(syncedTo, currentSlot, ourValidatorAddresses, publisher) {
905
+ */ async considerInvalidatingBlock(syncedTo, currentSlot) {
675
906
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
676
907
  if (pendingChainValidationStatus.valid) {
677
908
  return;
@@ -679,6 +910,7 @@ export { SequencerState };
679
910
  const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
680
911
  const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
681
912
  const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
913
+ const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
682
914
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
683
915
  const logData = {
684
916
  invalidL1Timestamp: invalidBlockTimestamp,
@@ -696,6 +928,21 @@ export { SequencerState };
696
928
  this.log.debug(`Not invalidating pending chain`, logData);
697
929
  return;
698
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);
699
946
  const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
700
947
  if (!invalidateBlock) {
701
948
  this.log.warn(`Failed to simulate invalidate block`, logData);
@@ -703,7 +950,12 @@ export { SequencerState };
703
950
  }
704
951
  this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
705
952
  publisher.enqueueInvalidateBlock(invalidateBlock);
706
- 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
+ }
707
959
  }
708
960
  getSlotStartBuildTimestamp(slotNumber) {
709
961
  return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
@@ -724,7 +976,7 @@ export { SequencerState };
724
976
  }
725
977
  _ts_decorate([
726
978
  trackSpan('Sequencer.work')
727
- ], Sequencer.prototype, "work", null);
979
+ ], Sequencer.prototype, "safeWork", null);
728
980
  _ts_decorate([
729
981
  trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables)=>({
730
982
  [Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber