@aztec/sequencer-client 0.0.1-commit.fce3e4f → 0.0.1-commit.fffb133c

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 (96) hide show
  1. package/dest/client/sequencer-client.d.ts +12 -12
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +32 -25
  4. package/dest/config.d.ts +12 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +73 -30
  7. package/dest/global_variable_builder/global_builder.d.ts +22 -13
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +51 -41
  10. package/dest/index.d.ts +2 -3
  11. package/dest/index.d.ts.map +1 -1
  12. package/dest/index.js +1 -2
  13. package/dest/publisher/config.d.ts +7 -4
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +9 -3
  16. package/dest/publisher/sequencer-publisher-factory.d.ts +5 -4
  17. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  18. package/dest/publisher/sequencer-publisher-factory.js +1 -1
  19. package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -3
  20. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher-metrics.js +15 -86
  22. package/dest/publisher/sequencer-publisher.d.ts +49 -41
  23. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher.js +566 -114
  25. package/dest/sequencer/checkpoint_proposal_job.d.ts +79 -0
  26. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  27. package/dest/sequencer/checkpoint_proposal_job.js +1165 -0
  28. package/dest/sequencer/checkpoint_voter.d.ts +35 -0
  29. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
  30. package/dest/sequencer/checkpoint_voter.js +109 -0
  31. package/dest/sequencer/config.d.ts +3 -2
  32. package/dest/sequencer/config.d.ts.map +1 -1
  33. package/dest/sequencer/events.d.ts +46 -0
  34. package/dest/sequencer/events.d.ts.map +1 -0
  35. package/dest/sequencer/events.js +1 -0
  36. package/dest/sequencer/index.d.ts +4 -2
  37. package/dest/sequencer/index.d.ts.map +1 -1
  38. package/dest/sequencer/index.js +3 -1
  39. package/dest/sequencer/metrics.d.ts +22 -2
  40. package/dest/sequencer/metrics.d.ts.map +1 -1
  41. package/dest/sequencer/metrics.js +125 -62
  42. package/dest/sequencer/sequencer.d.ts +107 -131
  43. package/dest/sequencer/sequencer.d.ts.map +1 -1
  44. package/dest/sequencer/sequencer.js +694 -605
  45. package/dest/sequencer/timetable.d.ts +54 -14
  46. package/dest/sequencer/timetable.d.ts.map +1 -1
  47. package/dest/sequencer/timetable.js +148 -59
  48. package/dest/sequencer/types.d.ts +3 -0
  49. package/dest/sequencer/types.d.ts.map +1 -0
  50. package/dest/sequencer/types.js +1 -0
  51. package/dest/sequencer/utils.d.ts +14 -8
  52. package/dest/sequencer/utils.d.ts.map +1 -1
  53. package/dest/sequencer/utils.js +7 -4
  54. package/dest/test/index.d.ts +4 -3
  55. package/dest/test/index.d.ts.map +1 -1
  56. package/dest/test/mock_checkpoint_builder.d.ts +95 -0
  57. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
  58. package/dest/test/mock_checkpoint_builder.js +222 -0
  59. package/dest/test/utils.d.ts +53 -0
  60. package/dest/test/utils.d.ts.map +1 -0
  61. package/dest/test/utils.js +103 -0
  62. package/package.json +32 -30
  63. package/src/client/sequencer-client.ts +30 -41
  64. package/src/config.ts +78 -34
  65. package/src/global_variable_builder/global_builder.ts +65 -61
  66. package/src/index.ts +1 -7
  67. package/src/publisher/config.ts +12 -9
  68. package/src/publisher/sequencer-publisher-factory.ts +5 -4
  69. package/src/publisher/sequencer-publisher-metrics.ts +16 -72
  70. package/src/publisher/sequencer-publisher.ts +269 -146
  71. package/src/sequencer/README.md +531 -0
  72. package/src/sequencer/checkpoint_proposal_job.ts +845 -0
  73. package/src/sequencer/checkpoint_voter.ts +130 -0
  74. package/src/sequencer/config.ts +2 -1
  75. package/src/sequencer/events.ts +27 -0
  76. package/src/sequencer/index.ts +3 -1
  77. package/src/sequencer/metrics.ts +164 -70
  78. package/src/sequencer/sequencer.ts +437 -812
  79. package/src/sequencer/timetable.ts +173 -79
  80. package/src/sequencer/types.ts +6 -0
  81. package/src/sequencer/utils.ts +18 -9
  82. package/src/test/index.ts +3 -2
  83. package/src/test/mock_checkpoint_builder.ts +311 -0
  84. package/src/test/utils.ts +164 -0
  85. package/dest/sequencer/block_builder.d.ts +0 -27
  86. package/dest/sequencer/block_builder.d.ts.map +0 -1
  87. package/dest/sequencer/block_builder.js +0 -134
  88. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  89. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  90. package/dest/tx_validator/nullifier_cache.js +0 -24
  91. package/dest/tx_validator/tx_validator_factory.d.ts +0 -17
  92. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  93. package/dest/tx_validator/tx_validator_factory.js +0 -53
  94. package/src/sequencer/block_builder.ts +0 -222
  95. package/src/tx_validator/nullifier_cache.ts +0 -30
  96. package/src/tx_validator/tx_validator_factory.ts +0 -132
@@ -1,44 +1,46 @@
1
- import { L2Block } from '@aztec/aztec.js/block';
1
+ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
- import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
4
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
+ import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
- FormattedViemError,
8
7
  type GovernanceProposerContract,
9
8
  type IEmpireBase,
10
- type L1BlobInputs,
11
- type L1ContractsConfig,
12
- type L1TxConfig,
13
- type L1TxRequest,
14
9
  MULTI_CALL_3_ADDRESS,
15
10
  Multicall3,
16
11
  RollupContract,
17
12
  type TallySlashingProposerContract,
18
- type TransactionStats,
19
13
  type ViemCommitteeAttestations,
20
14
  type ViemHeader,
15
+ } from '@aztec/ethereum/contracts';
16
+ import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
17
+ import {
18
+ type L1BlobInputs,
19
+ type L1TxConfig,
20
+ type L1TxRequest,
21
+ type TransactionStats,
21
22
  WEI_CONST,
22
- formatViemError,
23
- tryExtractEvent,
24
- } from '@aztec/ethereum';
23
+ } from '@aztec/ethereum/l1-tx-utils';
25
24
  import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
+ import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
26
26
  import { sumBigint } from '@aztec/foundation/bigint';
27
27
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
- import { SlotNumber } from '@aztec/foundation/branded-types';
28
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
29
+ import { pick } from '@aztec/foundation/collection';
30
+ import type { Fr } from '@aztec/foundation/curves/bn254';
29
31
  import { EthAddress } from '@aztec/foundation/eth-address';
30
32
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
31
- import type { Fr } from '@aztec/foundation/fields';
32
33
  import { type Logger, createLogger } from '@aztec/foundation/log';
33
34
  import { bufferToHex } from '@aztec/foundation/string';
34
35
  import { DateProvider, Timer } from '@aztec/foundation/timer';
35
36
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
36
37
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
37
- import { CommitteeAttestation, CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
38
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
+ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
38
40
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
39
41
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
40
- import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
41
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
42
+ import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
43
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
42
44
 
43
45
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
44
46
 
@@ -78,12 +80,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
78
80
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
79
81
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
80
82
 
81
- export type InvalidateBlockRequest = {
83
+ export type InvalidateCheckpointRequest = {
82
84
  request: L1TxRequest;
83
85
  reason: 'invalid-attestation' | 'insufficient-attestations';
84
86
  gasUsed: bigint;
85
- blockNumber: number;
86
- forcePendingBlockNumber: number;
87
+ checkpointNumber: CheckpointNumber;
88
+ forcePendingCheckpointNumber: CheckpointNumber;
87
89
  };
88
90
 
89
91
  interface RequestWithExpiry {
@@ -108,13 +110,18 @@ export class SequencerPublisher {
108
110
 
109
111
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
110
112
 
113
+ private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
114
+
111
115
  protected log: Logger;
112
116
  protected ethereumSlotDuration: bigint;
113
117
 
114
- private blobSinkClient: BlobSinkClientInterface;
118
+ private blobClient: BlobClientInterface;
115
119
 
116
120
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
117
121
  private proposerAddressForSimulation?: EthAddress;
122
+
123
+ /** L1 fee analyzer for fisherman mode */
124
+ private l1FeeAnalyzer?: L1FeeAnalyzer;
118
125
  // @note - with blobs, the below estimate seems too large.
119
126
  // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
120
127
  // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
@@ -132,13 +139,15 @@ export class SequencerPublisher {
132
139
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
133
140
  public slashFactoryContract: SlashFactoryContract;
134
141
 
142
+ public readonly tracer: Tracer;
143
+
135
144
  protected requests: RequestWithExpiry[] = [];
136
145
 
137
146
  constructor(
138
147
  private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
139
148
  deps: {
140
149
  telemetry?: TelemetryClient;
141
- blobSinkClient?: BlobSinkClientInterface;
150
+ blobClient: BlobClientInterface;
142
151
  l1TxUtils: L1TxUtilsWithBlobs;
143
152
  rollupContract: RollupContract;
144
153
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -156,11 +165,11 @@ export class SequencerPublisher {
156
165
  this.epochCache = deps.epochCache;
157
166
  this.lastActions = deps.lastActions;
158
167
 
159
- this.blobSinkClient =
160
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
168
+ this.blobClient = deps.blobClient;
161
169
 
162
170
  const telemetry = deps.telemetry ?? getTelemetryClient();
163
171
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
172
+ this.tracer = telemetry.getTracer('SequencerPublisher');
164
173
  this.l1TxUtils = deps.l1TxUtils;
165
174
 
166
175
  this.rollupContract = deps.rollupContract;
@@ -174,6 +183,15 @@ export class SequencerPublisher {
174
183
  this.slashingProposerContract = newSlashingProposer;
175
184
  });
176
185
  this.slashFactoryContract = deps.slashFactoryContract;
186
+
187
+ // Initialize L1 fee analyzer for fisherman mode
188
+ if (config.fishermanMode) {
189
+ this.l1FeeAnalyzer = new L1FeeAnalyzer(
190
+ this.l1TxUtils.client,
191
+ deps.dateProvider,
192
+ createLogger('sequencer:publisher:fee-analyzer'),
193
+ );
194
+ }
177
195
  }
178
196
 
179
197
  public getRollupContract(): RollupContract {
@@ -184,6 +202,13 @@ export class SequencerPublisher {
184
202
  return this.l1TxUtils.getSenderAddress();
185
203
  }
186
204
 
205
+ /**
206
+ * Gets the L1 fee analyzer instance (only available in fisherman mode)
207
+ */
208
+ public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
209
+ return this.l1FeeAnalyzer;
210
+ }
211
+
187
212
  /**
188
213
  * Sets the proposer address to use for simulations in fisherman mode.
189
214
  * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
@@ -211,6 +236,62 @@ export class SequencerPublisher {
211
236
  }
212
237
  }
213
238
 
239
+ /**
240
+ * Analyzes L1 fees for the pending requests without sending them.
241
+ * This is used in fisherman mode to validate fee calculations.
242
+ * @param l2SlotNumber - The L2 slot number for this analysis
243
+ * @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
244
+ * @returns The analysis result (incomplete until block mines), or undefined if no requests
245
+ */
246
+ public async analyzeL1Fees(
247
+ l2SlotNumber: SlotNumber,
248
+ onComplete?: (analysis: L1FeeAnalysisResult) => void,
249
+ ): Promise<L1FeeAnalysisResult | undefined> {
250
+ if (!this.l1FeeAnalyzer) {
251
+ this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
252
+ return undefined;
253
+ }
254
+
255
+ const requestsToAnalyze = [...this.requests];
256
+ if (requestsToAnalyze.length === 0) {
257
+ this.log.debug('No requests to analyze for L1 fees');
258
+ return undefined;
259
+ }
260
+
261
+ // Extract blob config from requests (if any)
262
+ const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
263
+ const blobConfig = blobConfigs[0];
264
+
265
+ // Get gas configs
266
+ const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
267
+ const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
268
+ const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
269
+
270
+ // Get the transaction requests
271
+ const l1Requests = requestsToAnalyze.map(r => r.request);
272
+
273
+ // Start the analysis
274
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
+ l2SlotNumber,
276
+ gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
277
+ l1Requests,
278
+ blobConfig,
279
+ onComplete,
280
+ );
281
+
282
+ this.log.info('Started L1 fee analysis', {
283
+ analysisId,
284
+ l2SlotNumber: l2SlotNumber.toString(),
285
+ requestCount: requestsToAnalyze.length,
286
+ hasBlobConfig: !!blobConfig,
287
+ gasLimit: gasLimit.toString(),
288
+ actions: requestsToAnalyze.map(r => r.action),
289
+ });
290
+
291
+ // Return the analysis result (will be incomplete until block mines)
292
+ return this.l1FeeAnalyzer.getAnalysis(analysisId);
293
+ }
294
+
214
295
  /**
215
296
  * Sends all requests that are still valid.
216
297
  * @returns one of:
@@ -218,10 +299,11 @@ export class SequencerPublisher {
218
299
  * - a receipt and errorMsg if it failed on L1
219
300
  * - undefined if no valid requests are found OR the tx failed to send.
220
301
  */
302
+ @trackSpan('SequencerPublisher.sendRequests')
221
303
  public async sendRequests() {
222
304
  const requestsToProcess = [...this.requests];
223
305
  this.requests = [];
224
- if (this.interrupted) {
306
+ if (this.interrupted || requestsToProcess.length === 0) {
225
307
  return undefined;
226
308
  }
227
309
  const currentL2Slot = this.getCurrentL2Slot();
@@ -335,14 +417,14 @@ export class SequencerPublisher {
335
417
  public canProposeAtNextEthBlock(
336
418
  tipArchive: Fr,
337
419
  msgSender: EthAddress,
338
- opts: { forcePendingBlockNumber?: number } = {},
420
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
339
421
  ) {
340
422
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
341
423
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
342
424
 
343
425
  return this.rollupContract
344
426
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
345
- forcePendingCheckpointNumber: opts.forcePendingBlockNumber,
427
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
346
428
  })
347
429
  .catch(err => {
348
430
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -361,7 +443,11 @@ export class SequencerPublisher {
361
443
  * It will throw if the block header is invalid.
362
444
  * @param header - The block header to validate
363
445
  */
364
- public async validateBlockHeader(header: CheckpointHeader, opts?: { forcePendingBlockNumber: number | undefined }) {
446
+ @trackSpan('SequencerPublisher.validateBlockHeader')
447
+ public async validateBlockHeader(
448
+ header: CheckpointHeader,
449
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
450
+ ): Promise<void> {
365
451
  const flags = { ignoreDA: true, ignoreSignatures: true };
366
452
 
367
453
  const args = [
@@ -370,12 +456,14 @@ export class SequencerPublisher {
370
456
  [], // no signers
371
457
  Signature.empty().toViemSignature(),
372
458
  `0x${'0'.repeat(64)}`, // 32 empty bytes
373
- header.contentCommitment.blobsHash.toString(),
459
+ header.blobsHash.toString(),
374
460
  flags,
375
461
  ] as const;
376
462
 
377
463
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
378
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingBlockNumber);
464
+ const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
465
+ opts?.forcePendingCheckpointNumber,
466
+ );
379
467
  let balance = 0n;
380
468
  if (this.config.fishermanMode) {
381
469
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -402,77 +490,90 @@ export class SequencerPublisher {
402
490
  }
403
491
 
404
492
  /**
405
- * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
406
- * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
493
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
494
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
407
495
  */
408
- public async simulateInvalidateBlock(
409
- validationResult: ValidateBlockResult,
410
- ): Promise<InvalidateBlockRequest | undefined> {
496
+ public async simulateInvalidateCheckpoint(
497
+ validationResult: ValidateCheckpointResult,
498
+ ): Promise<InvalidateCheckpointRequest | undefined> {
411
499
  if (validationResult.valid) {
412
500
  return undefined;
413
501
  }
414
502
 
415
- const { reason, block } = validationResult;
416
- const blockNumber = block.blockNumber;
417
- const logData = { ...block, reason };
503
+ const { reason, checkpoint } = validationResult;
504
+ const checkpointNumber = checkpoint.checkpointNumber;
505
+ const logData = { ...checkpoint, reason };
418
506
 
419
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
420
- if (currentBlockNumber < validationResult.block.blockNumber) {
507
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
508
+ if (currentCheckpointNumber < checkpointNumber) {
421
509
  this.log.verbose(
422
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
423
- { currentBlockNumber, ...logData },
510
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
511
+ { currentCheckpointNumber, ...logData },
424
512
  );
425
513
  return undefined;
426
514
  }
427
515
 
428
- const request = this.buildInvalidateBlockRequest(validationResult);
429
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
516
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
517
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
430
518
 
431
519
  try {
432
520
  const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
433
- this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
521
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
+ ...logData,
523
+ request,
524
+ gasUsed,
525
+ });
434
526
 
435
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
527
+ return {
528
+ request,
529
+ gasUsed,
530
+ checkpointNumber,
531
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
532
+ reason,
533
+ };
436
534
  } catch (err) {
437
535
  const viemError = formatViemError(err);
438
536
 
439
- // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
440
- // we can safely ignore it and return undefined so we go ahead with block building.
537
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
441
539
  if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
442
540
  this.log.verbose(
443
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
541
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
444
542
  { ...logData, request, error: viemError.message },
445
543
  );
446
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
447
- if (latestPendingBlockNumber < blockNumber) {
448
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
544
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
545
+ if (latestPendingCheckpointNumber < checkpointNumber) {
546
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
449
547
  return undefined;
450
548
  } else {
451
549
  this.log.error(
452
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
550
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
453
551
  viemError,
454
552
  logData,
455
553
  );
456
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
457
- cause: viemError,
458
- });
554
+ throw new Error(
555
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
556
+ {
557
+ cause: viemError,
558
+ },
559
+ );
459
560
  }
460
561
  }
461
562
 
462
- // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
463
- this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
464
- throw new Error(`Failed to simulate invalidate block ${blockNumber}`, { cause: viemError });
563
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
564
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
565
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
465
566
  }
466
567
  }
467
568
 
468
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
569
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
469
570
  if (validationResult.valid) {
470
- throw new Error('Cannot invalidate a valid block');
571
+ throw new Error('Cannot invalidate a valid checkpoint');
471
572
  }
472
573
 
473
- const { block, committee, reason } = validationResult;
474
- const logData = { ...block, reason };
475
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
574
+ const { checkpoint, committee, reason } = validationResult;
575
+ const logData = { ...checkpoint, reason };
576
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
476
577
 
477
578
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
478
579
  validationResult.attestations,
@@ -480,14 +581,14 @@ export class SequencerPublisher {
480
581
 
481
582
  if (reason === 'invalid-attestation') {
482
583
  return this.rollupContract.buildInvalidateBadAttestationRequest(
483
- block.blockNumber,
584
+ checkpoint.checkpointNumber,
484
585
  attestationsAndSigners,
485
586
  committee,
486
587
  validationResult.invalidIndex,
487
588
  );
488
589
  } else if (reason === 'insufficient-attestations') {
489
590
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
490
- block.blockNumber,
591
+ checkpoint.checkpointNumber,
491
592
  attestationsAndSigners,
492
593
  committee,
493
594
  );
@@ -497,45 +598,39 @@ export class SequencerPublisher {
497
598
  }
498
599
  }
499
600
 
500
- /**
501
- * @notice Will simulate `propose` to make sure that the block is valid for submission
502
- *
503
- * @dev Throws if unable to propose
504
- *
505
- * @param block - The block to propose
506
- * @param attestationData - The block's attestation data
507
- *
508
- */
509
- public async validateBlockForSubmission(
510
- block: L2Block,
601
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */
602
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
603
+ public async validateCheckpointForSubmission(
604
+ checkpoint: Checkpoint,
511
605
  attestationsAndSigners: CommitteeAttestationsAndSigners,
512
606
  attestationsAndSignersSignature: Signature,
513
- options: { forcePendingBlockNumber?: number },
607
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
514
608
  ): Promise<bigint> {
515
609
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
516
610
 
611
+ // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
517
612
  // If we have no attestations, we still need to provide the empty attestations
518
613
  // so that the committee is recalculated correctly
519
- const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
520
- if (ignoreSignatures) {
521
- const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
522
- if (!committee) {
523
- this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
524
- throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
525
- }
526
- attestationsAndSigners.attestations = committee.map(committeeMember =>
527
- CommitteeAttestation.fromAddress(committeeMember),
528
- );
529
- }
530
-
531
- const blobFields = block.getCheckpointBlobFields();
614
+ // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
615
+ // if (ignoreSignatures) {
616
+ // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
617
+ // if (!committee) {
618
+ // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
619
+ // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
620
+ // }
621
+ // attestationsAndSigners.attestations = committee.map(committeeMember =>
622
+ // CommitteeAttestation.fromAddress(committeeMember),
623
+ // );
624
+ // }
625
+
626
+ const blobFields = checkpoint.toBlobFields();
532
627
  const blobs = getBlobsPerL1Block(blobFields);
533
628
  const blobInput = getPrefixedEthBlobCommitments(blobs);
534
629
 
535
630
  const args = [
536
631
  {
537
- header: block.getCheckpointHeader().toViem(),
538
- archive: toHex(block.archive.root.toBuffer()),
632
+ header: checkpoint.header.toViem(),
633
+ archive: toHex(checkpoint.archive.root.toBuffer()),
539
634
  oracleInput: {
540
635
  feeAssetPriceModifier: 0n,
541
636
  },
@@ -573,10 +668,19 @@ export class SequencerPublisher {
573
668
  const round = await base.computeRound(slotNumber);
574
669
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
575
670
 
671
+ if (roundInfo.quorumReached) {
672
+ return false;
673
+ }
674
+
576
675
  if (roundInfo.lastSignalSlot >= slotNumber) {
577
676
  return false;
578
677
  }
579
678
 
679
+ if (await this.isPayloadEmpty(payload)) {
680
+ this.log.warn(`Skipping vote cast for payload with empty code`);
681
+ return false;
682
+ }
683
+
580
684
  const cachedLastVote = this.lastActions[signalType];
581
685
  this.lastActions[signalType] = slotNumber;
582
686
  const action = signalType;
@@ -619,14 +723,14 @@ export class SequencerPublisher {
619
723
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
620
724
  if (!success) {
621
725
  this.log.error(
622
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`,
726
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
623
727
  logData,
624
728
  );
625
729
  this.lastActions[signalType] = cachedLastVote;
626
730
  return false;
627
731
  } else {
628
732
  this.log.info(
629
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
733
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
630
734
  logData,
631
735
  );
632
736
  return true;
@@ -636,6 +740,17 @@ export class SequencerPublisher {
636
740
  return true;
637
741
  }
638
742
 
743
+ private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
744
+ const key = payload.toString();
745
+ const cached = this.isPayloadEmptyCache.get(key);
746
+ if (cached) {
747
+ return cached;
748
+ }
749
+ const isEmpty = !(await this.l1TxUtils.getCode(payload));
750
+ this.isPayloadEmptyCache.set(key, isEmpty);
751
+ return isEmpty;
752
+ }
753
+
639
754
  /**
640
755
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
641
756
  * @param slotNumber - The slot number to cast a signal for.
@@ -783,27 +898,21 @@ export class SequencerPublisher {
783
898
  return true;
784
899
  }
785
900
 
786
- /**
787
- * Proposes a L2 block on L1.
788
- *
789
- * @param block - L2 block to propose.
790
- * @returns True if the tx has been enqueued, throws otherwise. See #9315
791
- */
792
- public async enqueueProposeL2Block(
793
- block: L2Block,
901
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
902
+ public async enqueueProposeCheckpoint(
903
+ checkpoint: Checkpoint,
794
904
  attestationsAndSigners: CommitteeAttestationsAndSigners,
795
905
  attestationsAndSignersSignature: Signature,
796
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
797
- ): Promise<boolean> {
798
- const checkpointHeader = block.getCheckpointHeader();
906
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
907
+ ): Promise<void> {
908
+ const checkpointHeader = checkpoint.header;
799
909
 
800
- const blobFields = block.getCheckpointBlobFields();
910
+ const blobFields = checkpoint.toBlobFields();
801
911
  const blobs = getBlobsPerL1Block(blobFields);
802
912
 
803
913
  const proposeTxArgs = {
804
914
  header: checkpointHeader,
805
- archive: block.archive.root.toBuffer(),
806
- body: block.body.toBuffer(),
915
+ archive: checkpoint.archive.root.toBuffer(),
807
916
  blobs,
808
917
  attestationsAndSigners,
809
918
  attestationsAndSignersSignature,
@@ -817,22 +926,29 @@ export class SequencerPublisher {
817
926
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
818
927
  // make time consistency checks break.
819
928
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
820
- ts = await this.validateBlockForSubmission(block, attestationsAndSigners, attestationsAndSignersSignature, opts);
929
+ ts = await this.validateCheckpointForSubmission(
930
+ checkpoint,
931
+ attestationsAndSigners,
932
+ attestationsAndSignersSignature,
933
+ opts,
934
+ );
821
935
  } catch (err: any) {
822
- this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
823
- ...block.getStats(),
824
- slotNumber: block.header.globalVariables.slotNumber,
825
- forcePendingBlockNumber: opts.forcePendingBlockNumber,
936
+ this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
937
+ ...checkpoint.getStats(),
938
+ slotNumber: checkpoint.header.slotNumber,
939
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
826
940
  });
827
941
  throw err;
828
942
  }
829
943
 
830
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
831
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
832
- return true;
944
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
945
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
833
946
  }
834
947
 
835
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
948
+ public enqueueInvalidateCheckpoint(
949
+ request: InvalidateCheckpointRequest | undefined,
950
+ opts: { txTimeoutAt?: Date } = {},
951
+ ) {
836
952
  if (!request) {
837
953
  return;
838
954
  }
@@ -840,9 +956,9 @@ export class SequencerPublisher {
840
956
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
841
957
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
842
958
 
843
- const { gasUsed, blockNumber } = request;
844
- const logData = { gasUsed, blockNumber, gasLimit, opts };
845
- this.log.verbose(`Enqueuing invalidate block request`, logData);
959
+ const { gasUsed, checkpointNumber } = request;
960
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
961
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
846
962
  this.addRequest({
847
963
  action: `invalidate-by-${request.reason}`,
848
964
  request: request.request,
@@ -855,9 +971,9 @@ export class SequencerPublisher {
855
971
  result.receipt.status === 'success' &&
856
972
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
857
973
  if (!success) {
858
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
974
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
859
975
  } else {
860
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
976
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
861
977
  }
862
978
  return !!success;
863
979
  },
@@ -936,7 +1052,7 @@ export class SequencerPublisher {
936
1052
  private async prepareProposeTx(
937
1053
  encodedData: L1ProcessArgs,
938
1054
  timestamp: bigint,
939
- options: { forcePendingBlockNumber?: number },
1055
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
940
1056
  ) {
941
1057
  const kzg = Blob.getViemKzgInstance();
942
1058
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -1017,7 +1133,7 @@ export class SequencerPublisher {
1017
1133
  `0x${string}`,
1018
1134
  ],
1019
1135
  timestamp: bigint,
1020
- options: { forcePendingBlockNumber?: number },
1136
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1021
1137
  ) {
1022
1138
  const rollupData = encodeFunctionData({
1023
1139
  abi: RollupAbi,
@@ -1025,10 +1141,10 @@ export class SequencerPublisher {
1025
1141
  args,
1026
1142
  });
1027
1143
 
1028
- // override the pending block number if requested
1029
- const forcePendingBlockNumberStateDiff = (
1030
- options.forcePendingBlockNumber !== undefined
1031
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber)
1144
+ // override the pending checkpoint number if requested
1145
+ const forcePendingCheckpointNumberStateDiff = (
1146
+ options.forcePendingCheckpointNumber !== undefined
1147
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1032
1148
  : []
1033
1149
  ).flatMap(override => override.stateDiff ?? []);
1034
1150
 
@@ -1038,7 +1154,7 @@ export class SequencerPublisher {
1038
1154
  // @note we override checkBlob to false since blobs are not part simulate()
1039
1155
  stateDiff: [
1040
1156
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1041
- ...forcePendingBlockNumberStateDiff,
1157
+ ...forcePendingCheckpointNumberStateDiff,
1042
1158
  ],
1043
1159
  },
1044
1160
  ];
@@ -1090,11 +1206,12 @@ export class SequencerPublisher {
1090
1206
  }
1091
1207
 
1092
1208
  private async addProposeTx(
1093
- block: L2Block,
1209
+ checkpoint: Checkpoint,
1094
1210
  encodedData: L1ProcessArgs,
1095
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
1211
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1096
1212
  timestamp: bigint,
1097
1213
  ): Promise<void> {
1214
+ const slot = checkpoint.header.slotNumber;
1098
1215
  const timer = new Timer();
1099
1216
  const kzg = Blob.getViemKzgInstance();
1100
1217
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
@@ -1109,11 +1226,13 @@ export class SequencerPublisher {
1109
1226
  SequencerPublisher.MULTICALL_OVERHEAD_GAS_GUESS, // We issue the simulation against the rollup contract, so we need to account for the overhead of the multicall3
1110
1227
  );
1111
1228
 
1112
- // Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1113
- // tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
1114
- void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch(_err => {
1115
- this.log.error('Failed to send blobs to blob sink');
1116
- });
1229
+ // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1230
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
1231
+ void Promise.resolve().then(() =>
1232
+ this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
1233
+ this.log.error('Failed to send blobs to blob client');
1234
+ }),
1235
+ );
1117
1236
 
1118
1237
  return this.addRequest({
1119
1238
  action: 'propose',
@@ -1121,7 +1240,7 @@ export class SequencerPublisher {
1121
1240
  to: this.rollupContract.address,
1122
1241
  data: rollupData,
1123
1242
  },
1124
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1243
+ lastValidL2Slot: checkpoint.header.slotNumber,
1125
1244
  gasConfig: { ...opts, gasLimit },
1126
1245
  blobConfig: {
1127
1246
  blobs: encodedData.blobs.map(b => b.data),
@@ -1136,11 +1255,12 @@ export class SequencerPublisher {
1136
1255
  receipt &&
1137
1256
  receipt.status === 'success' &&
1138
1257
  tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1258
+
1139
1259
  if (success) {
1140
1260
  const endBlock = receipt.blockNumber;
1141
1261
  const inclusionBlocks = Number(endBlock - startBlock);
1142
1262
  const { calldataGas, calldataSize, sender } = stats!;
1143
- const publishStats: L1PublishBlockStats = {
1263
+ const publishStats: L1PublishCheckpointStats = {
1144
1264
  gasPrice: receipt.effectiveGasPrice,
1145
1265
  gasUsed: receipt.gasUsed,
1146
1266
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1149,23 +1269,26 @@ export class SequencerPublisher {
1149
1269
  calldataGas,
1150
1270
  calldataSize,
1151
1271
  sender,
1152
- ...block.getStats(),
1272
+ ...checkpoint.getStats(),
1153
1273
  eventName: 'rollup-published-to-l1',
1154
1274
  blobCount: encodedData.blobs.length,
1155
1275
  inclusionBlocks,
1156
1276
  };
1157
- this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats(), ...receipt });
1277
+ this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
1278
+ ...stats,
1279
+ ...checkpoint.getStats(),
1280
+ ...pick(receipt, 'transactionHash', 'blockHash'),
1281
+ });
1158
1282
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1159
1283
 
1160
1284
  return true;
1161
1285
  } else {
1162
1286
  this.metrics.recordFailedTx('process');
1163
- this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
1164
- ...block.getStats(),
1165
- receipt,
1166
- txHash: receipt.transactionHash,
1167
- slotNumber: block.header.globalVariables.slotNumber,
1168
- });
1287
+ this.log.error(
1288
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1289
+ undefined,
1290
+ { ...checkpoint.getStats(), ...receipt },
1291
+ );
1169
1292
  return false;
1170
1293
  }
1171
1294
  },