@aztec/sequencer-client 0.0.1-commit.9b94fc1 → 0.0.1-commit.a072138

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 +33 -26
  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 +23 -86
  22. package/dest/publisher/sequencer-publisher.d.ts +49 -42
  23. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher.js +602 -129
  25. package/dest/sequencer/checkpoint_proposal_job.d.ts +96 -0
  26. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  27. package/dest/sequencer/checkpoint_proposal_job.js +1192 -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 +23 -3
  40. package/dest/sequencer/metrics.d.ts.map +1 -1
  41. package/dest/sequencer/metrics.js +143 -70
  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 +220 -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 +31 -42
  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 +19 -71
  70. package/src/publisher/sequencer-publisher.ts +300 -161
  71. package/src/sequencer/README.md +531 -0
  72. package/src/sequencer/checkpoint_proposal_job.ts +874 -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 +190 -78
  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 +309 -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,47 @@
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
+ MAX_L1_TX_LIMIT,
22
+ type TransactionStats,
21
23
  WEI_CONST,
22
- formatViemError,
23
- tryExtractEvent,
24
- } from '@aztec/ethereum';
24
+ } from '@aztec/ethereum/l1-tx-utils';
25
25
  import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
27
  import { sumBigint } from '@aztec/foundation/bigint';
27
28
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
- import { SlotNumber } from '@aztec/foundation/branded-types';
29
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
30
+ import { pick } from '@aztec/foundation/collection';
31
+ import type { Fr } from '@aztec/foundation/curves/bn254';
29
32
  import { EthAddress } from '@aztec/foundation/eth-address';
30
33
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
31
- import type { Fr } from '@aztec/foundation/fields';
32
34
  import { type Logger, createLogger } from '@aztec/foundation/log';
33
35
  import { bufferToHex } from '@aztec/foundation/string';
34
36
  import { DateProvider, Timer } from '@aztec/foundation/timer';
35
37
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
36
38
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
37
- import { CommitteeAttestation, CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
39
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
40
+ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
38
41
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
39
42
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
40
- import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
41
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
43
+ import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
42
45
 
43
46
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
44
47
 
@@ -78,12 +81,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
78
81
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
79
82
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
80
83
 
81
- export type InvalidateBlockRequest = {
84
+ export type InvalidateCheckpointRequest = {
82
85
  request: L1TxRequest;
83
86
  reason: 'invalid-attestation' | 'insufficient-attestations';
84
87
  gasUsed: bigint;
85
- blockNumber: number;
86
- forcePendingBlockNumber: number;
88
+ checkpointNumber: CheckpointNumber;
89
+ forcePendingCheckpointNumber: CheckpointNumber;
87
90
  };
88
91
 
89
92
  interface RequestWithExpiry {
@@ -108,18 +111,18 @@ export class SequencerPublisher {
108
111
 
109
112
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
110
113
 
114
+ private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
115
+
111
116
  protected log: Logger;
112
117
  protected ethereumSlotDuration: bigint;
113
118
 
114
- private blobSinkClient: BlobSinkClientInterface;
119
+ private blobClient: BlobClientInterface;
115
120
 
116
121
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
117
122
  private proposerAddressForSimulation?: EthAddress;
118
- // @note - with blobs, the below estimate seems too large.
119
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
120
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
121
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
122
123
 
124
+ /** L1 fee analyzer for fisherman mode */
125
+ private l1FeeAnalyzer?: L1FeeAnalyzer;
123
126
  // A CALL to a cold address is 2700 gas
124
127
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
125
128
 
@@ -132,13 +135,15 @@ export class SequencerPublisher {
132
135
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
133
136
  public slashFactoryContract: SlashFactoryContract;
134
137
 
138
+ public readonly tracer: Tracer;
139
+
135
140
  protected requests: RequestWithExpiry[] = [];
136
141
 
137
142
  constructor(
138
143
  private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
139
144
  deps: {
140
145
  telemetry?: TelemetryClient;
141
- blobSinkClient?: BlobSinkClientInterface;
146
+ blobClient: BlobClientInterface;
142
147
  l1TxUtils: L1TxUtilsWithBlobs;
143
148
  rollupContract: RollupContract;
144
149
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -156,11 +161,11 @@ export class SequencerPublisher {
156
161
  this.epochCache = deps.epochCache;
157
162
  this.lastActions = deps.lastActions;
158
163
 
159
- this.blobSinkClient =
160
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
164
+ this.blobClient = deps.blobClient;
161
165
 
162
166
  const telemetry = deps.telemetry ?? getTelemetryClient();
163
167
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
168
+ this.tracer = telemetry.getTracer('SequencerPublisher');
164
169
  this.l1TxUtils = deps.l1TxUtils;
165
170
 
166
171
  this.rollupContract = deps.rollupContract;
@@ -174,6 +179,15 @@ export class SequencerPublisher {
174
179
  this.slashingProposerContract = newSlashingProposer;
175
180
  });
176
181
  this.slashFactoryContract = deps.slashFactoryContract;
182
+
183
+ // Initialize L1 fee analyzer for fisherman mode
184
+ if (config.fishermanMode) {
185
+ this.l1FeeAnalyzer = new L1FeeAnalyzer(
186
+ this.l1TxUtils.client,
187
+ deps.dateProvider,
188
+ createLogger('sequencer:publisher:fee-analyzer'),
189
+ );
190
+ }
177
191
  }
178
192
 
179
193
  public getRollupContract(): RollupContract {
@@ -184,6 +198,13 @@ export class SequencerPublisher {
184
198
  return this.l1TxUtils.getSenderAddress();
185
199
  }
186
200
 
201
+ /**
202
+ * Gets the L1 fee analyzer instance (only available in fisherman mode)
203
+ */
204
+ public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
205
+ return this.l1FeeAnalyzer;
206
+ }
207
+
187
208
  /**
188
209
  * Sets the proposer address to use for simulations in fisherman mode.
189
210
  * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
@@ -211,6 +232,62 @@ export class SequencerPublisher {
211
232
  }
212
233
  }
213
234
 
235
+ /**
236
+ * Analyzes L1 fees for the pending requests without sending them.
237
+ * This is used in fisherman mode to validate fee calculations.
238
+ * @param l2SlotNumber - The L2 slot number for this analysis
239
+ * @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
240
+ * @returns The analysis result (incomplete until block mines), or undefined if no requests
241
+ */
242
+ public async analyzeL1Fees(
243
+ l2SlotNumber: SlotNumber,
244
+ onComplete?: (analysis: L1FeeAnalysisResult) => void,
245
+ ): Promise<L1FeeAnalysisResult | undefined> {
246
+ if (!this.l1FeeAnalyzer) {
247
+ this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
248
+ return undefined;
249
+ }
250
+
251
+ const requestsToAnalyze = [...this.requests];
252
+ if (requestsToAnalyze.length === 0) {
253
+ this.log.debug('No requests to analyze for L1 fees');
254
+ return undefined;
255
+ }
256
+
257
+ // Extract blob config from requests (if any)
258
+ const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
259
+ const blobConfig = blobConfigs[0];
260
+
261
+ // Get gas configs
262
+ const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
263
+ const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
264
+ const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
265
+
266
+ // Get the transaction requests
267
+ const l1Requests = requestsToAnalyze.map(r => r.request);
268
+
269
+ // Start the analysis
270
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(
271
+ l2SlotNumber,
272
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
273
+ l1Requests,
274
+ blobConfig,
275
+ onComplete,
276
+ );
277
+
278
+ this.log.info('Started L1 fee analysis', {
279
+ analysisId,
280
+ l2SlotNumber: l2SlotNumber.toString(),
281
+ requestCount: requestsToAnalyze.length,
282
+ hasBlobConfig: !!blobConfig,
283
+ gasLimit: gasLimit.toString(),
284
+ actions: requestsToAnalyze.map(r => r.action),
285
+ });
286
+
287
+ // Return the analysis result (will be incomplete until block mines)
288
+ return this.l1FeeAnalyzer.getAnalysis(analysisId);
289
+ }
290
+
214
291
  /**
215
292
  * Sends all requests that are still valid.
216
293
  * @returns one of:
@@ -218,10 +295,11 @@ export class SequencerPublisher {
218
295
  * - a receipt and errorMsg if it failed on L1
219
296
  * - undefined if no valid requests are found OR the tx failed to send.
220
297
  */
298
+ @trackSpan('SequencerPublisher.sendRequests')
221
299
  public async sendRequests() {
222
300
  const requestsToProcess = [...this.requests];
223
301
  this.requests = [];
224
- if (this.interrupted) {
302
+ if (this.interrupted || requestsToProcess.length === 0) {
225
303
  return undefined;
226
304
  }
227
305
  const currentL2Slot = this.getCurrentL2Slot();
@@ -264,7 +342,16 @@ export class SequencerPublisher {
264
342
 
265
343
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
266
344
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
267
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
345
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
346
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
347
+ const maxGas = MAX_L1_TX_LIMIT;
348
+ if (gasLimit !== undefined && gasLimit > maxGas) {
349
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
350
+ requested: gasLimit,
351
+ capped: maxGas,
352
+ });
353
+ gasLimit = maxGas;
354
+ }
268
355
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
269
356
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
270
357
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -335,14 +422,14 @@ export class SequencerPublisher {
335
422
  public canProposeAtNextEthBlock(
336
423
  tipArchive: Fr,
337
424
  msgSender: EthAddress,
338
- opts: { forcePendingBlockNumber?: number } = {},
425
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
339
426
  ) {
340
427
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
341
428
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
342
429
 
343
430
  return this.rollupContract
344
431
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
345
- forcePendingCheckpointNumber: opts.forcePendingBlockNumber,
432
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
346
433
  })
347
434
  .catch(err => {
348
435
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -361,7 +448,11 @@ export class SequencerPublisher {
361
448
  * It will throw if the block header is invalid.
362
449
  * @param header - The block header to validate
363
450
  */
364
- public async validateBlockHeader(header: CheckpointHeader, opts?: { forcePendingBlockNumber: number | undefined }) {
451
+ @trackSpan('SequencerPublisher.validateBlockHeader')
452
+ public async validateBlockHeader(
453
+ header: CheckpointHeader,
454
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
455
+ ): Promise<void> {
365
456
  const flags = { ignoreDA: true, ignoreSignatures: true };
366
457
 
367
458
  const args = [
@@ -370,12 +461,14 @@ export class SequencerPublisher {
370
461
  [], // no signers
371
462
  Signature.empty().toViemSignature(),
372
463
  `0x${'0'.repeat(64)}`, // 32 empty bytes
373
- header.contentCommitment.blobsHash.toString(),
464
+ header.blobsHash.toString(),
374
465
  flags,
375
466
  ] as const;
376
467
 
377
468
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
378
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingBlockNumber);
469
+ const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
470
+ opts?.forcePendingCheckpointNumber,
471
+ );
379
472
  let balance = 0n;
380
473
  if (this.config.fishermanMode) {
381
474
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -402,77 +495,95 @@ export class SequencerPublisher {
402
495
  }
403
496
 
404
497
  /**
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)
498
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
499
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
407
500
  */
408
- public async simulateInvalidateBlock(
409
- validationResult: ValidateBlockResult,
410
- ): Promise<InvalidateBlockRequest | undefined> {
501
+ public async simulateInvalidateCheckpoint(
502
+ validationResult: ValidateCheckpointResult,
503
+ ): Promise<InvalidateCheckpointRequest | undefined> {
411
504
  if (validationResult.valid) {
412
505
  return undefined;
413
506
  }
414
507
 
415
- const { reason, block } = validationResult;
416
- const blockNumber = block.blockNumber;
417
- const logData = { ...block, reason };
508
+ const { reason, checkpoint } = validationResult;
509
+ const checkpointNumber = checkpoint.checkpointNumber;
510
+ const logData = { ...checkpoint, reason };
418
511
 
419
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
420
- if (currentBlockNumber < validationResult.block.blockNumber) {
512
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
513
+ if (currentCheckpointNumber < checkpointNumber) {
421
514
  this.log.verbose(
422
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
423
- { currentBlockNumber, ...logData },
515
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
516
+ { currentCheckpointNumber, ...logData },
424
517
  );
425
518
  return undefined;
426
519
  }
427
520
 
428
- const request = this.buildInvalidateBlockRequest(validationResult);
429
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
521
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
522
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
430
523
 
431
524
  try {
432
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
433
- this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
525
+ const { gasUsed } = await this.l1TxUtils.simulate(
526
+ request,
527
+ undefined,
528
+ undefined,
529
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
530
+ );
531
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
532
+ ...logData,
533
+ request,
534
+ gasUsed,
535
+ });
434
536
 
435
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
537
+ return {
538
+ request,
539
+ gasUsed,
540
+ checkpointNumber,
541
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
542
+ reason,
543
+ };
436
544
  } catch (err) {
437
545
  const viemError = formatViemError(err);
438
546
 
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.
441
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
547
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
548
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
549
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
442
550
  this.log.verbose(
443
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
551
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
444
552
  { ...logData, request, error: viemError.message },
445
553
  );
446
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
447
- if (latestPendingBlockNumber < blockNumber) {
448
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
554
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
555
+ if (latestPendingCheckpointNumber < checkpointNumber) {
556
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
449
557
  return undefined;
450
558
  } else {
451
559
  this.log.error(
452
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
560
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
453
561
  viemError,
454
562
  logData,
455
563
  );
456
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
457
- cause: viemError,
458
- });
564
+ throw new Error(
565
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
566
+ {
567
+ cause: viemError,
568
+ },
569
+ );
459
570
  }
460
571
  }
461
572
 
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 });
573
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
575
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
465
576
  }
466
577
  }
467
578
 
468
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
579
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
469
580
  if (validationResult.valid) {
470
- throw new Error('Cannot invalidate a valid block');
581
+ throw new Error('Cannot invalidate a valid checkpoint');
471
582
  }
472
583
 
473
- const { block, committee, reason } = validationResult;
474
- const logData = { ...block, reason };
475
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
584
+ const { checkpoint, committee, reason } = validationResult;
585
+ const logData = { ...checkpoint, reason };
586
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
476
587
 
477
588
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
478
589
  validationResult.attestations,
@@ -480,14 +591,14 @@ export class SequencerPublisher {
480
591
 
481
592
  if (reason === 'invalid-attestation') {
482
593
  return this.rollupContract.buildInvalidateBadAttestationRequest(
483
- block.blockNumber,
594
+ checkpoint.checkpointNumber,
484
595
  attestationsAndSigners,
485
596
  committee,
486
597
  validationResult.invalidIndex,
487
598
  );
488
599
  } else if (reason === 'insufficient-attestations') {
489
600
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
490
- block.blockNumber,
601
+ checkpoint.checkpointNumber,
491
602
  attestationsAndSigners,
492
603
  committee,
493
604
  );
@@ -497,45 +608,39 @@ export class SequencerPublisher {
497
608
  }
498
609
  }
499
610
 
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,
611
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */
612
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
613
+ public async validateCheckpointForSubmission(
614
+ checkpoint: Checkpoint,
511
615
  attestationsAndSigners: CommitteeAttestationsAndSigners,
512
616
  attestationsAndSignersSignature: Signature,
513
- options: { forcePendingBlockNumber?: number },
617
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
514
618
  ): Promise<bigint> {
515
619
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
516
620
 
621
+ // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
517
622
  // If we have no attestations, we still need to provide the empty attestations
518
623
  // 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();
624
+ // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
625
+ // if (ignoreSignatures) {
626
+ // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
627
+ // if (!committee) {
628
+ // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
629
+ // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
630
+ // }
631
+ // attestationsAndSigners.attestations = committee.map(committeeMember =>
632
+ // CommitteeAttestation.fromAddress(committeeMember),
633
+ // );
634
+ // }
635
+
636
+ const blobFields = checkpoint.toBlobFields();
532
637
  const blobs = getBlobsPerL1Block(blobFields);
533
638
  const blobInput = getPrefixedEthBlobCommitments(blobs);
534
639
 
535
640
  const args = [
536
641
  {
537
- header: block.getCheckpointHeader().toViem(),
538
- archive: toHex(block.archive.root.toBuffer()),
642
+ header: checkpoint.header.toViem(),
643
+ archive: toHex(checkpoint.archive.root.toBuffer()),
539
644
  oracleInput: {
540
645
  feeAssetPriceModifier: 0n,
541
646
  },
@@ -573,10 +678,19 @@ export class SequencerPublisher {
573
678
  const round = await base.computeRound(slotNumber);
574
679
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
575
680
 
681
+ if (roundInfo.quorumReached) {
682
+ return false;
683
+ }
684
+
576
685
  if (roundInfo.lastSignalSlot >= slotNumber) {
577
686
  return false;
578
687
  }
579
688
 
689
+ if (await this.isPayloadEmpty(payload)) {
690
+ this.log.warn(`Skipping vote cast for payload with empty code`);
691
+ return false;
692
+ }
693
+
580
694
  const cachedLastVote = this.lastActions[signalType];
581
695
  this.lastActions[signalType] = slotNumber;
582
696
  const action = signalType;
@@ -596,7 +710,7 @@ export class SequencerPublisher {
596
710
  });
597
711
 
598
712
  try {
599
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
713
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
600
714
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
601
715
  } catch (err) {
602
716
  this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
@@ -619,14 +733,14 @@ export class SequencerPublisher {
619
733
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
620
734
  if (!success) {
621
735
  this.log.error(
622
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`,
736
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
623
737
  logData,
624
738
  );
625
739
  this.lastActions[signalType] = cachedLastVote;
626
740
  return false;
627
741
  } else {
628
742
  this.log.info(
629
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
743
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
630
744
  logData,
631
745
  );
632
746
  return true;
@@ -636,6 +750,17 @@ export class SequencerPublisher {
636
750
  return true;
637
751
  }
638
752
 
753
+ private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
754
+ const key = payload.toString();
755
+ const cached = this.isPayloadEmptyCache.get(key);
756
+ if (cached) {
757
+ return cached;
758
+ }
759
+ const isEmpty = !(await this.l1TxUtils.getCode(payload));
760
+ this.isPayloadEmptyCache.set(key, isEmpty);
761
+ return isEmpty;
762
+ }
763
+
639
764
  /**
640
765
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
641
766
  * @param slotNumber - The slot number to cast a signal for.
@@ -783,27 +908,21 @@ export class SequencerPublisher {
783
908
  return true;
784
909
  }
785
910
 
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,
911
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
912
+ public async enqueueProposeCheckpoint(
913
+ checkpoint: Checkpoint,
794
914
  attestationsAndSigners: CommitteeAttestationsAndSigners,
795
915
  attestationsAndSignersSignature: Signature,
796
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
797
- ): Promise<boolean> {
798
- const checkpointHeader = block.getCheckpointHeader();
916
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
917
+ ): Promise<void> {
918
+ const checkpointHeader = checkpoint.header;
799
919
 
800
- const blobFields = block.getCheckpointBlobFields();
920
+ const blobFields = checkpoint.toBlobFields();
801
921
  const blobs = getBlobsPerL1Block(blobFields);
802
922
 
803
923
  const proposeTxArgs = {
804
924
  header: checkpointHeader,
805
- archive: block.archive.root.toBuffer(),
806
- body: block.body.toBuffer(),
925
+ archive: checkpoint.archive.root.toBuffer(),
807
926
  blobs,
808
927
  attestationsAndSigners,
809
928
  attestationsAndSignersSignature,
@@ -817,22 +936,29 @@ export class SequencerPublisher {
817
936
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
818
937
  // make time consistency checks break.
819
938
  // 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);
939
+ ts = await this.validateCheckpointForSubmission(
940
+ checkpoint,
941
+ attestationsAndSigners,
942
+ attestationsAndSignersSignature,
943
+ opts,
944
+ );
821
945
  } 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,
946
+ this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
947
+ ...checkpoint.getStats(),
948
+ slotNumber: checkpoint.header.slotNumber,
949
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
826
950
  });
827
951
  throw err;
828
952
  }
829
953
 
830
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
831
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
832
- return true;
954
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
955
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
833
956
  }
834
957
 
835
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
958
+ public enqueueInvalidateCheckpoint(
959
+ request: InvalidateCheckpointRequest | undefined,
960
+ opts: { txTimeoutAt?: Date } = {},
961
+ ) {
836
962
  if (!request) {
837
963
  return;
838
964
  }
@@ -840,9 +966,9 @@ export class SequencerPublisher {
840
966
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
841
967
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
842
968
 
843
- const { gasUsed, blockNumber } = request;
844
- const logData = { gasUsed, blockNumber, gasLimit, opts };
845
- this.log.verbose(`Enqueuing invalidate block request`, logData);
969
+ const { gasUsed, checkpointNumber } = request;
970
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
971
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
846
972
  this.addRequest({
847
973
  action: `invalidate-by-${request.reason}`,
848
974
  request: request.request,
@@ -855,9 +981,9 @@ export class SequencerPublisher {
855
981
  result.receipt.status === 'success' &&
856
982
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
857
983
  if (!success) {
858
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
984
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
859
985
  } else {
860
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
986
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
861
987
  }
862
988
  return !!success;
863
989
  },
@@ -883,12 +1009,14 @@ export class SequencerPublisher {
883
1009
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
884
1010
 
885
1011
  let gasUsed: bigint;
1012
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
886
1013
  try {
887
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1014
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
888
1015
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
889
1016
  } catch (err) {
890
- const viemError = formatViemError(err);
1017
+ const viemError = formatViemError(err, simulateAbi);
891
1018
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
+
892
1020
  return false;
893
1021
  }
894
1022
 
@@ -896,10 +1024,14 @@ export class SequencerPublisher {
896
1024
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
897
1025
  logData.gasLimit = gasLimit;
898
1026
 
1027
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1028
+ // when the tx is sent and a revert is diagnosed via simulation.
1029
+ const requestWithAbi = { ...request, abi: simulateAbi };
1030
+
899
1031
  this.log.debug(`Enqueuing ${action}`, logData);
900
1032
  this.addRequest({
901
1033
  action,
902
- request,
1034
+ request: requestWithAbi,
903
1035
  gasConfig: { gasLimit },
904
1036
  lastValidL2Slot: slotNumber,
905
1037
  checkSuccess: (_req, result) => {
@@ -936,7 +1068,7 @@ export class SequencerPublisher {
936
1068
  private async prepareProposeTx(
937
1069
  encodedData: L1ProcessArgs,
938
1070
  timestamp: bigint,
939
- options: { forcePendingBlockNumber?: number },
1071
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
940
1072
  ) {
941
1073
  const kzg = Blob.getViemKzgInstance();
942
1074
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -1017,7 +1149,7 @@ export class SequencerPublisher {
1017
1149
  `0x${string}`,
1018
1150
  ],
1019
1151
  timestamp: bigint,
1020
- options: { forcePendingBlockNumber?: number },
1152
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1021
1153
  ) {
1022
1154
  const rollupData = encodeFunctionData({
1023
1155
  abi: RollupAbi,
@@ -1025,10 +1157,10 @@ export class SequencerPublisher {
1025
1157
  args,
1026
1158
  });
1027
1159
 
1028
- // override the pending block number if requested
1029
- const forcePendingBlockNumberStateDiff = (
1030
- options.forcePendingBlockNumber !== undefined
1031
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber)
1160
+ // override the pending checkpoint number if requested
1161
+ const forcePendingCheckpointNumberStateDiff = (
1162
+ options.forcePendingCheckpointNumber !== undefined
1163
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1032
1164
  : []
1033
1165
  ).flatMap(override => override.stateDiff ?? []);
1034
1166
 
@@ -1038,7 +1170,7 @@ export class SequencerPublisher {
1038
1170
  // @note we override checkBlob to false since blobs are not part simulate()
1039
1171
  stateDiff: [
1040
1172
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1041
- ...forcePendingBlockNumberStateDiff,
1173
+ ...forcePendingCheckpointNumberStateDiff,
1042
1174
  ],
1043
1175
  },
1044
1176
  ];
@@ -1055,20 +1187,20 @@ export class SequencerPublisher {
1055
1187
  {
1056
1188
  to: this.rollupContract.address,
1057
1189
  data: rollupData,
1058
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1190
+ gas: MAX_L1_TX_LIMIT,
1059
1191
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1060
1192
  },
1061
1193
  {
1062
1194
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1063
1195
  time: timestamp + 1n,
1064
1196
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1065
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1197
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1066
1198
  },
1067
1199
  stateOverrides,
1068
1200
  RollupAbi,
1069
1201
  {
1070
1202
  // @note fallback gas estimate to use if the node doesn't support simulation API
1071
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1203
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1072
1204
  },
1073
1205
  )
1074
1206
  .catch(err => {
@@ -1078,7 +1210,7 @@ export class SequencerPublisher {
1078
1210
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1079
1211
  // Return a minimal simulation result with the fallback gas estimate
1080
1212
  return {
1081
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1213
+ gasUsed: MAX_L1_TX_LIMIT,
1082
1214
  logs: [],
1083
1215
  };
1084
1216
  }
@@ -1090,11 +1222,12 @@ export class SequencerPublisher {
1090
1222
  }
1091
1223
 
1092
1224
  private async addProposeTx(
1093
- block: L2Block,
1225
+ checkpoint: Checkpoint,
1094
1226
  encodedData: L1ProcessArgs,
1095
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
1227
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1096
1228
  timestamp: bigint,
1097
1229
  ): Promise<void> {
1230
+ const slot = checkpoint.header.slotNumber;
1098
1231
  const timer = new Timer();
1099
1232
  const kzg = Blob.getViemKzgInstance();
1100
1233
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
@@ -1109,11 +1242,13 @@ export class SequencerPublisher {
1109
1242
  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
1243
  );
1111
1244
 
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
- });
1245
+ // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1246
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
1247
+ void Promise.resolve().then(() =>
1248
+ this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
1249
+ this.log.error('Failed to send blobs to blob client');
1250
+ }),
1251
+ );
1117
1252
 
1118
1253
  return this.addRequest({
1119
1254
  action: 'propose',
@@ -1121,7 +1256,7 @@ export class SequencerPublisher {
1121
1256
  to: this.rollupContract.address,
1122
1257
  data: rollupData,
1123
1258
  },
1124
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1259
+ lastValidL2Slot: checkpoint.header.slotNumber,
1125
1260
  gasConfig: { ...opts, gasLimit },
1126
1261
  blobConfig: {
1127
1262
  blobs: encodedData.blobs.map(b => b.data),
@@ -1136,11 +1271,12 @@ export class SequencerPublisher {
1136
1271
  receipt &&
1137
1272
  receipt.status === 'success' &&
1138
1273
  tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1274
+
1139
1275
  if (success) {
1140
1276
  const endBlock = receipt.blockNumber;
1141
1277
  const inclusionBlocks = Number(endBlock - startBlock);
1142
1278
  const { calldataGas, calldataSize, sender } = stats!;
1143
- const publishStats: L1PublishBlockStats = {
1279
+ const publishStats: L1PublishCheckpointStats = {
1144
1280
  gasPrice: receipt.effectiveGasPrice,
1145
1281
  gasUsed: receipt.gasUsed,
1146
1282
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1149,23 +1285,26 @@ export class SequencerPublisher {
1149
1285
  calldataGas,
1150
1286
  calldataSize,
1151
1287
  sender,
1152
- ...block.getStats(),
1288
+ ...checkpoint.getStats(),
1153
1289
  eventName: 'rollup-published-to-l1',
1154
1290
  blobCount: encodedData.blobs.length,
1155
1291
  inclusionBlocks,
1156
1292
  };
1157
- this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats(), ...receipt });
1293
+ this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
1294
+ ...stats,
1295
+ ...checkpoint.getStats(),
1296
+ ...pick(receipt, 'transactionHash', 'blockHash'),
1297
+ });
1158
1298
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1159
1299
 
1160
1300
  return true;
1161
1301
  } else {
1162
1302
  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
- });
1303
+ this.log.error(
1304
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1305
+ undefined,
1306
+ { ...checkpoint.getStats(), ...receipt },
1307
+ );
1169
1308
  return false;
1170
1309
  }
1171
1310
  },