@aztec/sequencer-client 0.0.1-commit.d3ec352c → 0.0.1-commit.e6bd8901

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 +21 -12
  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 +48 -40
  23. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher.js +564 -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 +1164 -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 +690 -602
  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 +92 -0
  57. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
  58. package/dest/test/mock_checkpoint_builder.js +208 -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 +30 -28
  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 +63 -59
  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 +262 -155
  71. package/src/sequencer/README.md +531 -0
  72. package/src/sequencer/checkpoint_proposal_job.ts +843 -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 +430 -804
  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 +295 -0
  84. package/src/test/utils.ts +164 -0
  85. package/dest/sequencer/block_builder.d.ts +0 -28
  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 -18
  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 -133
@@ -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 { BlockNumber, CheckpointNumber, 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: BlockNumber;
86
- forcePendingBlockNumber: BlockNumber;
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,17 +417,14 @@ export class SequencerPublisher {
335
417
  public canProposeAtNextEthBlock(
336
418
  tipArchive: Fr,
337
419
  msgSender: EthAddress,
338
- opts: { forcePendingBlockNumber?: BlockNumber } = {},
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:
346
- opts.forcePendingBlockNumber !== undefined
347
- ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
348
- : undefined,
427
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
349
428
  })
350
429
  .catch(err => {
351
430
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -364,10 +443,11 @@ export class SequencerPublisher {
364
443
  * It will throw if the block header is invalid.
365
444
  * @param header - The block header to validate
366
445
  */
446
+ @trackSpan('SequencerPublisher.validateBlockHeader')
367
447
  public async validateBlockHeader(
368
448
  header: CheckpointHeader,
369
- opts?: { forcePendingBlockNumber: BlockNumber | undefined },
370
- ) {
449
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
450
+ ): Promise<void> {
371
451
  const flags = { ignoreDA: true, ignoreSignatures: true };
372
452
 
373
453
  const args = [
@@ -376,17 +456,13 @@ export class SequencerPublisher {
376
456
  [], // no signers
377
457
  Signature.empty().toViemSignature(),
378
458
  `0x${'0'.repeat(64)}`, // 32 empty bytes
379
- header.contentCommitment.blobsHash.toString(),
459
+ header.blobsHash.toString(),
380
460
  flags,
381
461
  ] as const;
382
462
 
383
463
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
384
- const optsForcePendingCheckpointNumber =
385
- opts?.forcePendingBlockNumber !== undefined
386
- ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
387
- : undefined;
388
464
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
389
- optsForcePendingCheckpointNumber,
465
+ opts?.forcePendingCheckpointNumber,
390
466
  );
391
467
  let balance = 0n;
392
468
  if (this.config.fishermanMode) {
@@ -414,77 +490,90 @@ export class SequencerPublisher {
414
490
  }
415
491
 
416
492
  /**
417
- * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
418
- * @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)
419
495
  */
420
- public async simulateInvalidateBlock(
421
- validationResult: ValidateBlockResult,
422
- ): Promise<InvalidateBlockRequest | undefined> {
496
+ public async simulateInvalidateCheckpoint(
497
+ validationResult: ValidateCheckpointResult,
498
+ ): Promise<InvalidateCheckpointRequest | undefined> {
423
499
  if (validationResult.valid) {
424
500
  return undefined;
425
501
  }
426
502
 
427
- const { reason, block } = validationResult;
428
- const blockNumber = block.blockNumber;
429
- const logData = { ...block, reason };
503
+ const { reason, checkpoint } = validationResult;
504
+ const checkpointNumber = checkpoint.checkpointNumber;
505
+ const logData = { ...checkpoint, reason };
430
506
 
431
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
432
- if (currentBlockNumber < validationResult.block.blockNumber) {
507
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
508
+ if (currentCheckpointNumber < checkpointNumber) {
433
509
  this.log.verbose(
434
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
435
- { currentBlockNumber, ...logData },
510
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
511
+ { currentCheckpointNumber, ...logData },
436
512
  );
437
513
  return undefined;
438
514
  }
439
515
 
440
- const request = this.buildInvalidateBlockRequest(validationResult);
441
- 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 });
442
518
 
443
519
  try {
444
520
  const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
445
- 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
+ });
446
526
 
447
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: BlockNumber(blockNumber - 1), reason };
527
+ return {
528
+ request,
529
+ gasUsed,
530
+ checkpointNumber,
531
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
532
+ reason,
533
+ };
448
534
  } catch (err) {
449
535
  const viemError = formatViemError(err);
450
536
 
451
- // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
452
- // 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.
453
539
  if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
454
540
  this.log.verbose(
455
- `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`,
456
542
  { ...logData, request, error: viemError.message },
457
543
  );
458
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
459
- if (latestPendingBlockNumber < blockNumber) {
460
- 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 });
461
547
  return undefined;
462
548
  } else {
463
549
  this.log.error(
464
- `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`,
465
551
  viemError,
466
552
  logData,
467
553
  );
468
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
469
- cause: viemError,
470
- });
554
+ throw new Error(
555
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
556
+ {
557
+ cause: viemError,
558
+ },
559
+ );
471
560
  }
472
561
  }
473
562
 
474
- // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
475
- this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
476
- 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 });
477
566
  }
478
567
  }
479
568
 
480
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
569
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
481
570
  if (validationResult.valid) {
482
- throw new Error('Cannot invalidate a valid block');
571
+ throw new Error('Cannot invalidate a valid checkpoint');
483
572
  }
484
573
 
485
- const { block, committee, reason } = validationResult;
486
- const logData = { ...block, reason };
487
- 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);
488
577
 
489
578
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
490
579
  validationResult.attestations,
@@ -492,14 +581,14 @@ export class SequencerPublisher {
492
581
 
493
582
  if (reason === 'invalid-attestation') {
494
583
  return this.rollupContract.buildInvalidateBadAttestationRequest(
495
- CheckpointNumber.fromBlockNumber(block.blockNumber),
584
+ checkpoint.checkpointNumber,
496
585
  attestationsAndSigners,
497
586
  committee,
498
587
  validationResult.invalidIndex,
499
588
  );
500
589
  } else if (reason === 'insufficient-attestations') {
501
590
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
502
- CheckpointNumber.fromBlockNumber(block.blockNumber),
591
+ checkpoint.checkpointNumber,
503
592
  attestationsAndSigners,
504
593
  committee,
505
594
  );
@@ -509,45 +598,39 @@ export class SequencerPublisher {
509
598
  }
510
599
  }
511
600
 
512
- /**
513
- * @notice Will simulate `propose` to make sure that the block is valid for submission
514
- *
515
- * @dev Throws if unable to propose
516
- *
517
- * @param block - The block to propose
518
- * @param attestationData - The block's attestation data
519
- *
520
- */
521
- public async validateBlockForSubmission(
522
- 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,
523
605
  attestationsAndSigners: CommitteeAttestationsAndSigners,
524
606
  attestationsAndSignersSignature: Signature,
525
- options: { forcePendingBlockNumber?: BlockNumber },
607
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
526
608
  ): Promise<bigint> {
527
609
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
528
610
 
611
+ // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
529
612
  // If we have no attestations, we still need to provide the empty attestations
530
613
  // so that the committee is recalculated correctly
531
- const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
532
- if (ignoreSignatures) {
533
- const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
534
- if (!committee) {
535
- this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
536
- throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
537
- }
538
- attestationsAndSigners.attestations = committee.map(committeeMember =>
539
- CommitteeAttestation.fromAddress(committeeMember),
540
- );
541
- }
542
-
543
- 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();
544
627
  const blobs = getBlobsPerL1Block(blobFields);
545
628
  const blobInput = getPrefixedEthBlobCommitments(blobs);
546
629
 
547
630
  const args = [
548
631
  {
549
- header: block.getCheckpointHeader().toViem(),
550
- archive: toHex(block.archive.root.toBuffer()),
632
+ header: checkpoint.header.toViem(),
633
+ archive: toHex(checkpoint.archive.root.toBuffer()),
551
634
  oracleInput: {
552
635
  feeAssetPriceModifier: 0n,
553
636
  },
@@ -585,10 +668,19 @@ export class SequencerPublisher {
585
668
  const round = await base.computeRound(slotNumber);
586
669
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
587
670
 
671
+ if (roundInfo.quorumReached) {
672
+ return false;
673
+ }
674
+
588
675
  if (roundInfo.lastSignalSlot >= slotNumber) {
589
676
  return false;
590
677
  }
591
678
 
679
+ if (await this.isPayloadEmpty(payload)) {
680
+ this.log.warn(`Skipping vote cast for payload with empty code`);
681
+ return false;
682
+ }
683
+
592
684
  const cachedLastVote = this.lastActions[signalType];
593
685
  this.lastActions[signalType] = slotNumber;
594
686
  const action = signalType;
@@ -631,14 +723,14 @@ export class SequencerPublisher {
631
723
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
632
724
  if (!success) {
633
725
  this.log.error(
634
- `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`,
635
727
  logData,
636
728
  );
637
729
  this.lastActions[signalType] = cachedLastVote;
638
730
  return false;
639
731
  } else {
640
732
  this.log.info(
641
- `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`,
642
734
  logData,
643
735
  );
644
736
  return true;
@@ -648,6 +740,17 @@ export class SequencerPublisher {
648
740
  return true;
649
741
  }
650
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
+
651
754
  /**
652
755
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
653
756
  * @param slotNumber - The slot number to cast a signal for.
@@ -795,27 +898,21 @@ export class SequencerPublisher {
795
898
  return true;
796
899
  }
797
900
 
798
- /**
799
- * Proposes a L2 block on L1.
800
- *
801
- * @param block - L2 block to propose.
802
- * @returns True if the tx has been enqueued, throws otherwise. See #9315
803
- */
804
- public async enqueueProposeL2Block(
805
- block: L2Block,
901
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
902
+ public async enqueueProposeCheckpoint(
903
+ checkpoint: Checkpoint,
806
904
  attestationsAndSigners: CommitteeAttestationsAndSigners,
807
905
  attestationsAndSignersSignature: Signature,
808
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
809
- ): Promise<boolean> {
810
- const checkpointHeader = block.getCheckpointHeader();
906
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
907
+ ): Promise<void> {
908
+ const checkpointHeader = checkpoint.header;
811
909
 
812
- const blobFields = block.getCheckpointBlobFields();
910
+ const blobFields = checkpoint.toBlobFields();
813
911
  const blobs = getBlobsPerL1Block(blobFields);
814
912
 
815
913
  const proposeTxArgs = {
816
914
  header: checkpointHeader,
817
- archive: block.archive.root.toBuffer(),
818
- body: block.body.toBuffer(),
915
+ archive: checkpoint.archive.root.toBuffer(),
819
916
  blobs,
820
917
  attestationsAndSigners,
821
918
  attestationsAndSignersSignature,
@@ -829,22 +926,29 @@ export class SequencerPublisher {
829
926
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
830
927
  // make time consistency checks break.
831
928
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
832
- ts = await this.validateBlockForSubmission(block, attestationsAndSigners, attestationsAndSignersSignature, opts);
929
+ ts = await this.validateCheckpointForSubmission(
930
+ checkpoint,
931
+ attestationsAndSigners,
932
+ attestationsAndSignersSignature,
933
+ opts,
934
+ );
833
935
  } catch (err: any) {
834
- this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
835
- ...block.getStats(),
836
- slotNumber: block.header.globalVariables.slotNumber,
837
- 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,
838
940
  });
839
941
  throw err;
840
942
  }
841
943
 
842
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
843
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
844
- return true;
944
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
945
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
845
946
  }
846
947
 
847
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
948
+ public enqueueInvalidateCheckpoint(
949
+ request: InvalidateCheckpointRequest | undefined,
950
+ opts: { txTimeoutAt?: Date } = {},
951
+ ) {
848
952
  if (!request) {
849
953
  return;
850
954
  }
@@ -852,9 +956,9 @@ export class SequencerPublisher {
852
956
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
853
957
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
854
958
 
855
- const { gasUsed, blockNumber } = request;
856
- const logData = { gasUsed, blockNumber, gasLimit, opts };
857
- 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);
858
962
  this.addRequest({
859
963
  action: `invalidate-by-${request.reason}`,
860
964
  request: request.request,
@@ -867,9 +971,9 @@ export class SequencerPublisher {
867
971
  result.receipt.status === 'success' &&
868
972
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
869
973
  if (!success) {
870
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
974
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
871
975
  } else {
872
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
976
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
873
977
  }
874
978
  return !!success;
875
979
  },
@@ -948,7 +1052,7 @@ export class SequencerPublisher {
948
1052
  private async prepareProposeTx(
949
1053
  encodedData: L1ProcessArgs,
950
1054
  timestamp: bigint,
951
- options: { forcePendingBlockNumber?: BlockNumber },
1055
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
952
1056
  ) {
953
1057
  const kzg = Blob.getViemKzgInstance();
954
1058
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -1029,7 +1133,7 @@ export class SequencerPublisher {
1029
1133
  `0x${string}`,
1030
1134
  ],
1031
1135
  timestamp: bigint,
1032
- options: { forcePendingBlockNumber?: BlockNumber },
1136
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1033
1137
  ) {
1034
1138
  const rollupData = encodeFunctionData({
1035
1139
  abi: RollupAbi,
@@ -1038,13 +1142,9 @@ export class SequencerPublisher {
1038
1142
  });
1039
1143
 
1040
1144
  // override the pending checkpoint number if requested
1041
- const optsForcePendingCheckpointNumber =
1042
- options.forcePendingBlockNumber !== undefined
1043
- ? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber)
1044
- : undefined;
1045
1145
  const forcePendingCheckpointNumberStateDiff = (
1046
- optsForcePendingCheckpointNumber !== undefined
1047
- ? await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber)
1146
+ options.forcePendingCheckpointNumber !== undefined
1147
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1048
1148
  : []
1049
1149
  ).flatMap(override => override.stateDiff ?? []);
1050
1150
 
@@ -1106,11 +1206,12 @@ export class SequencerPublisher {
1106
1206
  }
1107
1207
 
1108
1208
  private async addProposeTx(
1109
- block: L2Block,
1209
+ checkpoint: Checkpoint,
1110
1210
  encodedData: L1ProcessArgs,
1111
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1211
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1112
1212
  timestamp: bigint,
1113
1213
  ): Promise<void> {
1214
+ const slot = checkpoint.header.slotNumber;
1114
1215
  const timer = new Timer();
1115
1216
  const kzg = Blob.getViemKzgInstance();
1116
1217
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
@@ -1125,11 +1226,13 @@ export class SequencerPublisher {
1125
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
1126
1227
  );
1127
1228
 
1128
- // Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1129
- // tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
1130
- void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch(_err => {
1131
- this.log.error('Failed to send blobs to blob sink');
1132
- });
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
+ );
1133
1236
 
1134
1237
  return this.addRequest({
1135
1238
  action: 'propose',
@@ -1137,7 +1240,7 @@ export class SequencerPublisher {
1137
1240
  to: this.rollupContract.address,
1138
1241
  data: rollupData,
1139
1242
  },
1140
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1243
+ lastValidL2Slot: checkpoint.header.slotNumber,
1141
1244
  gasConfig: { ...opts, gasLimit },
1142
1245
  blobConfig: {
1143
1246
  blobs: encodedData.blobs.map(b => b.data),
@@ -1152,11 +1255,12 @@ export class SequencerPublisher {
1152
1255
  receipt &&
1153
1256
  receipt.status === 'success' &&
1154
1257
  tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1258
+
1155
1259
  if (success) {
1156
1260
  const endBlock = receipt.blockNumber;
1157
1261
  const inclusionBlocks = Number(endBlock - startBlock);
1158
1262
  const { calldataGas, calldataSize, sender } = stats!;
1159
- const publishStats: L1PublishBlockStats = {
1263
+ const publishStats: L1PublishCheckpointStats = {
1160
1264
  gasPrice: receipt.effectiveGasPrice,
1161
1265
  gasUsed: receipt.gasUsed,
1162
1266
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1165,23 +1269,26 @@ export class SequencerPublisher {
1165
1269
  calldataGas,
1166
1270
  calldataSize,
1167
1271
  sender,
1168
- ...block.getStats(),
1272
+ ...checkpoint.getStats(),
1169
1273
  eventName: 'rollup-published-to-l1',
1170
1274
  blobCount: encodedData.blobs.length,
1171
1275
  inclusionBlocks,
1172
1276
  };
1173
- 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
+ });
1174
1282
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1175
1283
 
1176
1284
  return true;
1177
1285
  } else {
1178
1286
  this.metrics.recordFailedTx('process');
1179
- this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
1180
- ...block.getStats(),
1181
- receipt,
1182
- txHash: receipt.transactionHash,
1183
- slotNumber: block.header.globalVariables.slotNumber,
1184
- });
1287
+ this.log.error(
1288
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1289
+ undefined,
1290
+ { ...checkpoint.getStats(), ...receipt },
1291
+ );
1185
1292
  return false;
1186
1293
  }
1187
1294
  },