@aztec/sequencer-client 0.0.1-commit.21caa21 → 0.0.1-commit.21ecf947b

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 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +71 -32
  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 +56 -42
  23. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher.js +615 -133
  25. package/dest/sequencer/checkpoint_proposal_job.d.ts +102 -0
  26. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  27. package/dest/sequencer/checkpoint_proposal_job.js +1193 -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 +27 -3
  40. package/dest/sequencer/metrics.d.ts.map +1 -1
  41. package/dest/sequencer/metrics.js +188 -66
  42. package/dest/sequencer/sequencer.d.ts +109 -131
  43. package/dest/sequencer/sequencer.d.ts.map +1 -1
  44. package/dest/sequencer/sequencer.js +700 -606
  45. package/dest/sequencer/timetable.d.ts +51 -14
  46. package/dest/sequencer/timetable.d.ts.map +1 -1
  47. package/dest/sequencer/timetable.js +145 -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 +97 -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 +104 -0
  62. package/package.json +32 -30
  63. package/src/client/sequencer-client.ts +31 -42
  64. package/src/config.ts +78 -36
  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 +327 -166
  71. package/src/sequencer/README.md +531 -0
  72. package/src/sequencer/checkpoint_proposal_job.ts +882 -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 +236 -76
  78. package/src/sequencer/sequencer.ts +445 -813
  79. package/src/sequencer/timetable.ts +175 -80
  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 +320 -0
  84. package/src/test/utils.ts +167 -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,48 @@
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,
7
+ FeeAssetPriceOracle,
8
8
  type GovernanceProposerContract,
9
9
  type IEmpireBase,
10
- type L1BlobInputs,
11
- type L1ContractsConfig,
12
- type L1TxConfig,
13
- type L1TxRequest,
14
10
  MULTI_CALL_3_ADDRESS,
15
11
  Multicall3,
16
12
  RollupContract,
17
13
  type TallySlashingProposerContract,
18
- type TransactionStats,
19
14
  type ViemCommitteeAttestations,
20
15
  type ViemHeader,
16
+ } from '@aztec/ethereum/contracts';
17
+ import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
18
+ import {
19
+ type L1BlobInputs,
20
+ type L1TxConfig,
21
+ type L1TxRequest,
22
+ MAX_L1_TX_LIMIT,
23
+ type TransactionStats,
21
24
  WEI_CONST,
22
- formatViemError,
23
- tryExtractEvent,
24
- } from '@aztec/ethereum';
25
+ } from '@aztec/ethereum/l1-tx-utils';
25
26
  import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
27
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
28
  import { sumBigint } from '@aztec/foundation/bigint';
27
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
- import { SlotNumber } from '@aztec/foundation/branded-types';
30
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { pick } from '@aztec/foundation/collection';
32
+ import type { Fr } from '@aztec/foundation/curves/bn254';
29
33
  import { EthAddress } from '@aztec/foundation/eth-address';
30
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
31
- import type { Fr } from '@aztec/foundation/fields';
32
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
33
36
  import { bufferToHex } from '@aztec/foundation/string';
34
37
  import { DateProvider, Timer } from '@aztec/foundation/timer';
35
38
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
36
39
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
37
- import { CommitteeAttestation, CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
40
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
41
+ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
38
42
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
39
43
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
40
- import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
41
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
44
+ import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
45
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
42
46
 
43
47
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
44
48
 
@@ -57,6 +61,8 @@ type L1ProcessArgs = {
57
61
  attestationsAndSigners: CommitteeAttestationsAndSigners;
58
62
  /** Attestations and signers signature */
59
63
  attestationsAndSignersSignature: Signature;
64
+ /** The fee asset price modifier in basis points (from oracle) */
65
+ feeAssetPriceModifier: bigint;
60
66
  };
61
67
 
62
68
  export const Actions = [
@@ -78,12 +84,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
78
84
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
79
85
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
80
86
 
81
- export type InvalidateBlockRequest = {
87
+ export type InvalidateCheckpointRequest = {
82
88
  request: L1TxRequest;
83
89
  reason: 'invalid-attestation' | 'insufficient-attestations';
84
90
  gasUsed: bigint;
85
- blockNumber: number;
86
- forcePendingBlockNumber: number;
91
+ checkpointNumber: CheckpointNumber;
92
+ forcePendingCheckpointNumber: CheckpointNumber;
87
93
  };
88
94
 
89
95
  interface RequestWithExpiry {
@@ -108,17 +114,21 @@ export class SequencerPublisher {
108
114
 
109
115
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
110
116
 
117
+ private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
118
+
111
119
  protected log: Logger;
112
120
  protected ethereumSlotDuration: bigint;
113
121
 
114
- private blobSinkClient: BlobSinkClientInterface;
122
+ private blobClient: BlobClientInterface;
115
123
 
116
124
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
117
125
  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;
126
+
127
+ /** L1 fee analyzer for fisherman mode */
128
+ private l1FeeAnalyzer?: L1FeeAnalyzer;
129
+
130
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
131
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
122
132
 
123
133
  // A CALL to a cold address is 2700 gas
124
134
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -132,13 +142,15 @@ export class SequencerPublisher {
132
142
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
133
143
  public slashFactoryContract: SlashFactoryContract;
134
144
 
145
+ public readonly tracer: Tracer;
146
+
135
147
  protected requests: RequestWithExpiry[] = [];
136
148
 
137
149
  constructor(
138
150
  private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
139
151
  deps: {
140
152
  telemetry?: TelemetryClient;
141
- blobSinkClient?: BlobSinkClientInterface;
153
+ blobClient: BlobClientInterface;
142
154
  l1TxUtils: L1TxUtilsWithBlobs;
143
155
  rollupContract: RollupContract;
144
156
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -156,11 +168,11 @@ export class SequencerPublisher {
156
168
  this.epochCache = deps.epochCache;
157
169
  this.lastActions = deps.lastActions;
158
170
 
159
- this.blobSinkClient =
160
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
171
+ this.blobClient = deps.blobClient;
161
172
 
162
173
  const telemetry = deps.telemetry ?? getTelemetryClient();
163
174
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
175
+ this.tracer = telemetry.getTracer('SequencerPublisher');
164
176
  this.l1TxUtils = deps.l1TxUtils;
165
177
 
166
178
  this.rollupContract = deps.rollupContract;
@@ -174,16 +186,47 @@ export class SequencerPublisher {
174
186
  this.slashingProposerContract = newSlashingProposer;
175
187
  });
176
188
  this.slashFactoryContract = deps.slashFactoryContract;
189
+
190
+ // Initialize L1 fee analyzer for fisherman mode
191
+ if (config.fishermanMode) {
192
+ this.l1FeeAnalyzer = new L1FeeAnalyzer(
193
+ this.l1TxUtils.client,
194
+ deps.dateProvider,
195
+ createLogger('sequencer:publisher:fee-analyzer'),
196
+ );
197
+ }
198
+
199
+ // Initialize fee asset price oracle
200
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
201
+ this.l1TxUtils.client,
202
+ this.rollupContract,
203
+ createLogger('sequencer:publisher:price-oracle'),
204
+ );
177
205
  }
178
206
 
179
207
  public getRollupContract(): RollupContract {
180
208
  return this.rollupContract;
181
209
  }
182
210
 
211
+ /**
212
+ * Gets the fee asset price modifier from the oracle.
213
+ * Returns 0n if the oracle query fails.
214
+ */
215
+ public getFeeAssetPriceModifier(): Promise<bigint> {
216
+ return this.feeAssetPriceOracle.computePriceModifier();
217
+ }
218
+
183
219
  public getSenderAddress() {
184
220
  return this.l1TxUtils.getSenderAddress();
185
221
  }
186
222
 
223
+ /**
224
+ * Gets the L1 fee analyzer instance (only available in fisherman mode)
225
+ */
226
+ public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
227
+ return this.l1FeeAnalyzer;
228
+ }
229
+
187
230
  /**
188
231
  * Sets the proposer address to use for simulations in fisherman mode.
189
232
  * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
@@ -211,6 +254,62 @@ export class SequencerPublisher {
211
254
  }
212
255
  }
213
256
 
257
+ /**
258
+ * Analyzes L1 fees for the pending requests without sending them.
259
+ * This is used in fisherman mode to validate fee calculations.
260
+ * @param l2SlotNumber - The L2 slot number for this analysis
261
+ * @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
262
+ * @returns The analysis result (incomplete until block mines), or undefined if no requests
263
+ */
264
+ public async analyzeL1Fees(
265
+ l2SlotNumber: SlotNumber,
266
+ onComplete?: (analysis: L1FeeAnalysisResult) => void,
267
+ ): Promise<L1FeeAnalysisResult | undefined> {
268
+ if (!this.l1FeeAnalyzer) {
269
+ this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
270
+ return undefined;
271
+ }
272
+
273
+ const requestsToAnalyze = [...this.requests];
274
+ if (requestsToAnalyze.length === 0) {
275
+ this.log.debug('No requests to analyze for L1 fees');
276
+ return undefined;
277
+ }
278
+
279
+ // Extract blob config from requests (if any)
280
+ const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
281
+ const blobConfig = blobConfigs[0];
282
+
283
+ // Get gas configs
284
+ const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
285
+ const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
286
+ const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
287
+
288
+ // Get the transaction requests
289
+ const l1Requests = requestsToAnalyze.map(r => r.request);
290
+
291
+ // Start the analysis
292
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(
293
+ l2SlotNumber,
294
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
295
+ l1Requests,
296
+ blobConfig,
297
+ onComplete,
298
+ );
299
+
300
+ this.log.info('Started L1 fee analysis', {
301
+ analysisId,
302
+ l2SlotNumber: l2SlotNumber.toString(),
303
+ requestCount: requestsToAnalyze.length,
304
+ hasBlobConfig: !!blobConfig,
305
+ gasLimit: gasLimit.toString(),
306
+ actions: requestsToAnalyze.map(r => r.action),
307
+ });
308
+
309
+ // Return the analysis result (will be incomplete until block mines)
310
+ return this.l1FeeAnalyzer.getAnalysis(analysisId);
311
+ }
312
+
214
313
  /**
215
314
  * Sends all requests that are still valid.
216
315
  * @returns one of:
@@ -218,10 +317,11 @@ export class SequencerPublisher {
218
317
  * - a receipt and errorMsg if it failed on L1
219
318
  * - undefined if no valid requests are found OR the tx failed to send.
220
319
  */
320
+ @trackSpan('SequencerPublisher.sendRequests')
221
321
  public async sendRequests() {
222
322
  const requestsToProcess = [...this.requests];
223
323
  this.requests = [];
224
- if (this.interrupted) {
324
+ if (this.interrupted || requestsToProcess.length === 0) {
225
325
  return undefined;
226
326
  }
227
327
  const currentL2Slot = this.getCurrentL2Slot();
@@ -264,7 +364,16 @@ export class SequencerPublisher {
264
364
 
265
365
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
266
366
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
267
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
367
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
368
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
369
+ const maxGas = MAX_L1_TX_LIMIT;
370
+ if (gasLimit !== undefined && gasLimit > maxGas) {
371
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
372
+ requested: gasLimit,
373
+ capped: maxGas,
374
+ });
375
+ gasLimit = maxGas;
376
+ }
268
377
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
269
378
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
270
379
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -335,14 +444,14 @@ export class SequencerPublisher {
335
444
  public canProposeAtNextEthBlock(
336
445
  tipArchive: Fr,
337
446
  msgSender: EthAddress,
338
- opts: { forcePendingBlockNumber?: number } = {},
447
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
339
448
  ) {
340
449
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
341
450
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
342
451
 
343
452
  return this.rollupContract
344
453
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
345
- forcePendingCheckpointNumber: opts.forcePendingBlockNumber,
454
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
346
455
  })
347
456
  .catch(err => {
348
457
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -361,7 +470,11 @@ export class SequencerPublisher {
361
470
  * It will throw if the block header is invalid.
362
471
  * @param header - The block header to validate
363
472
  */
364
- public async validateBlockHeader(header: CheckpointHeader, opts?: { forcePendingBlockNumber: number | undefined }) {
473
+ @trackSpan('SequencerPublisher.validateBlockHeader')
474
+ public async validateBlockHeader(
475
+ header: CheckpointHeader,
476
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
477
+ ): Promise<void> {
365
478
  const flags = { ignoreDA: true, ignoreSignatures: true };
366
479
 
367
480
  const args = [
@@ -370,12 +483,14 @@ export class SequencerPublisher {
370
483
  [], // no signers
371
484
  Signature.empty().toViemSignature(),
372
485
  `0x${'0'.repeat(64)}`, // 32 empty bytes
373
- header.contentCommitment.blobsHash.toString(),
486
+ header.blobsHash.toString(),
374
487
  flags,
375
488
  ] as const;
376
489
 
377
490
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
378
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingBlockNumber);
491
+ const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
492
+ opts?.forcePendingCheckpointNumber,
493
+ );
379
494
  let balance = 0n;
380
495
  if (this.config.fishermanMode) {
381
496
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -402,77 +517,95 @@ export class SequencerPublisher {
402
517
  }
403
518
 
404
519
  /**
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)
520
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
521
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
407
522
  */
408
- public async simulateInvalidateBlock(
409
- validationResult: ValidateBlockResult,
410
- ): Promise<InvalidateBlockRequest | undefined> {
523
+ public async simulateInvalidateCheckpoint(
524
+ validationResult: ValidateCheckpointResult,
525
+ ): Promise<InvalidateCheckpointRequest | undefined> {
411
526
  if (validationResult.valid) {
412
527
  return undefined;
413
528
  }
414
529
 
415
- const { reason, block } = validationResult;
416
- const blockNumber = block.blockNumber;
417
- const logData = { ...block, reason };
530
+ const { reason, checkpoint } = validationResult;
531
+ const checkpointNumber = checkpoint.checkpointNumber;
532
+ const logData = { ...checkpoint, reason };
418
533
 
419
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
420
- if (currentBlockNumber < validationResult.block.blockNumber) {
534
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
535
+ if (currentCheckpointNumber < checkpointNumber) {
421
536
  this.log.verbose(
422
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
423
- { currentBlockNumber, ...logData },
537
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
538
+ { currentCheckpointNumber, ...logData },
424
539
  );
425
540
  return undefined;
426
541
  }
427
542
 
428
- const request = this.buildInvalidateBlockRequest(validationResult);
429
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
543
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
544
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
430
545
 
431
546
  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 });
547
+ const { gasUsed } = await this.l1TxUtils.simulate(
548
+ request,
549
+ undefined,
550
+ undefined,
551
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
552
+ );
553
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
554
+ ...logData,
555
+ request,
556
+ gasUsed,
557
+ });
434
558
 
435
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
559
+ return {
560
+ request,
561
+ gasUsed,
562
+ checkpointNumber,
563
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
564
+ reason,
565
+ };
436
566
  } catch (err) {
437
567
  const viemError = formatViemError(err);
438
568
 
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')) {
569
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
570
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
571
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
442
572
  this.log.verbose(
443
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
573
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
444
574
  { ...logData, request, error: viemError.message },
445
575
  );
446
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
447
- if (latestPendingBlockNumber < blockNumber) {
448
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
576
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
577
+ if (latestPendingCheckpointNumber < checkpointNumber) {
578
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
449
579
  return undefined;
450
580
  } else {
451
581
  this.log.error(
452
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
582
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
453
583
  viemError,
454
584
  logData,
455
585
  );
456
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
457
- cause: viemError,
458
- });
586
+ throw new Error(
587
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
588
+ {
589
+ cause: viemError,
590
+ },
591
+ );
459
592
  }
460
593
  }
461
594
 
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 });
595
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
596
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
597
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
465
598
  }
466
599
  }
467
600
 
468
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
601
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
469
602
  if (validationResult.valid) {
470
- throw new Error('Cannot invalidate a valid block');
603
+ throw new Error('Cannot invalidate a valid checkpoint');
471
604
  }
472
605
 
473
- const { block, committee, reason } = validationResult;
474
- const logData = { ...block, reason };
475
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
606
+ const { checkpoint, committee, reason } = validationResult;
607
+ const logData = { ...checkpoint, reason };
608
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
476
609
 
477
610
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
478
611
  validationResult.attestations,
@@ -480,14 +613,14 @@ export class SequencerPublisher {
480
613
 
481
614
  if (reason === 'invalid-attestation') {
482
615
  return this.rollupContract.buildInvalidateBadAttestationRequest(
483
- block.blockNumber,
616
+ checkpoint.checkpointNumber,
484
617
  attestationsAndSigners,
485
618
  committee,
486
619
  validationResult.invalidIndex,
487
620
  );
488
621
  } else if (reason === 'insufficient-attestations') {
489
622
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
490
- block.blockNumber,
623
+ checkpoint.checkpointNumber,
491
624
  attestationsAndSigners,
492
625
  committee,
493
626
  );
@@ -497,47 +630,41 @@ export class SequencerPublisher {
497
630
  }
498
631
  }
499
632
 
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,
633
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */
634
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
635
+ public async validateCheckpointForSubmission(
636
+ checkpoint: Checkpoint,
511
637
  attestationsAndSigners: CommitteeAttestationsAndSigners,
512
638
  attestationsAndSignersSignature: Signature,
513
- options: { forcePendingBlockNumber?: number },
639
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
514
640
  ): Promise<bigint> {
515
641
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
516
642
 
643
+ // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
517
644
  // If we have no attestations, we still need to provide the empty attestations
518
645
  // 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();
646
+ // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
647
+ // if (ignoreSignatures) {
648
+ // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
649
+ // if (!committee) {
650
+ // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
651
+ // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
652
+ // }
653
+ // attestationsAndSigners.attestations = committee.map(committeeMember =>
654
+ // CommitteeAttestation.fromAddress(committeeMember),
655
+ // );
656
+ // }
657
+
658
+ const blobFields = checkpoint.toBlobFields();
532
659
  const blobs = getBlobsPerL1Block(blobFields);
533
660
  const blobInput = getPrefixedEthBlobCommitments(blobs);
534
661
 
535
662
  const args = [
536
663
  {
537
- header: block.getCheckpointHeader().toViem(),
538
- archive: toHex(block.archive.root.toBuffer()),
664
+ header: checkpoint.header.toViem(),
665
+ archive: toHex(checkpoint.archive.root.toBuffer()),
539
666
  oracleInput: {
540
- feeAssetPriceModifier: 0n,
667
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
541
668
  },
542
669
  },
543
670
  attestationsAndSigners.getPackedAttestations(),
@@ -573,10 +700,19 @@ export class SequencerPublisher {
573
700
  const round = await base.computeRound(slotNumber);
574
701
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
575
702
 
703
+ if (roundInfo.quorumReached) {
704
+ return false;
705
+ }
706
+
576
707
  if (roundInfo.lastSignalSlot >= slotNumber) {
577
708
  return false;
578
709
  }
579
710
 
711
+ if (await this.isPayloadEmpty(payload)) {
712
+ this.log.warn(`Skipping vote cast for payload with empty code`);
713
+ return false;
714
+ }
715
+
580
716
  const cachedLastVote = this.lastActions[signalType];
581
717
  this.lastActions[signalType] = slotNumber;
582
718
  const action = signalType;
@@ -596,7 +732,7 @@ export class SequencerPublisher {
596
732
  });
597
733
 
598
734
  try {
599
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
735
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
600
736
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
601
737
  } catch (err) {
602
738
  this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
@@ -619,14 +755,14 @@ export class SequencerPublisher {
619
755
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
620
756
  if (!success) {
621
757
  this.log.error(
622
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`,
758
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
623
759
  logData,
624
760
  );
625
761
  this.lastActions[signalType] = cachedLastVote;
626
762
  return false;
627
763
  } else {
628
764
  this.log.info(
629
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
765
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
630
766
  logData,
631
767
  );
632
768
  return true;
@@ -636,6 +772,17 @@ export class SequencerPublisher {
636
772
  return true;
637
773
  }
638
774
 
775
+ private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
776
+ const key = payload.toString();
777
+ const cached = this.isPayloadEmptyCache.get(key);
778
+ if (cached) {
779
+ return cached;
780
+ }
781
+ const isEmpty = !(await this.l1TxUtils.getCode(payload));
782
+ this.isPayloadEmptyCache.set(key, isEmpty);
783
+ return isEmpty;
784
+ }
785
+
639
786
  /**
640
787
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
641
788
  * @param slotNumber - The slot number to cast a signal for.
@@ -783,30 +930,25 @@ export class SequencerPublisher {
783
930
  return true;
784
931
  }
785
932
 
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,
933
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
934
+ public async enqueueProposeCheckpoint(
935
+ checkpoint: Checkpoint,
794
936
  attestationsAndSigners: CommitteeAttestationsAndSigners,
795
937
  attestationsAndSignersSignature: Signature,
796
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
797
- ): Promise<boolean> {
798
- const checkpointHeader = block.getCheckpointHeader();
938
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
939
+ ): Promise<void> {
940
+ const checkpointHeader = checkpoint.header;
799
941
 
800
- const blobFields = block.getCheckpointBlobFields();
942
+ const blobFields = checkpoint.toBlobFields();
801
943
  const blobs = getBlobsPerL1Block(blobFields);
802
944
 
803
- const proposeTxArgs = {
945
+ const proposeTxArgs: L1ProcessArgs = {
804
946
  header: checkpointHeader,
805
- archive: block.archive.root.toBuffer(),
806
- body: block.body.toBuffer(),
947
+ archive: checkpoint.archive.root.toBuffer(),
807
948
  blobs,
808
949
  attestationsAndSigners,
809
950
  attestationsAndSignersSignature,
951
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
810
952
  };
811
953
 
812
954
  let ts: bigint;
@@ -817,22 +959,29 @@ export class SequencerPublisher {
817
959
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
818
960
  // make time consistency checks break.
819
961
  // 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);
962
+ ts = await this.validateCheckpointForSubmission(
963
+ checkpoint,
964
+ attestationsAndSigners,
965
+ attestationsAndSignersSignature,
966
+ opts,
967
+ );
821
968
  } 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,
969
+ this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
970
+ ...checkpoint.getStats(),
971
+ slotNumber: checkpoint.header.slotNumber,
972
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
826
973
  });
827
974
  throw err;
828
975
  }
829
976
 
830
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
831
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
832
- return true;
977
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
978
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
833
979
  }
834
980
 
835
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
981
+ public enqueueInvalidateCheckpoint(
982
+ request: InvalidateCheckpointRequest | undefined,
983
+ opts: { txTimeoutAt?: Date } = {},
984
+ ) {
836
985
  if (!request) {
837
986
  return;
838
987
  }
@@ -840,9 +989,9 @@ export class SequencerPublisher {
840
989
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
841
990
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
842
991
 
843
- const { gasUsed, blockNumber } = request;
844
- const logData = { gasUsed, blockNumber, gasLimit, opts };
845
- this.log.verbose(`Enqueuing invalidate block request`, logData);
992
+ const { gasUsed, checkpointNumber } = request;
993
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
994
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
846
995
  this.addRequest({
847
996
  action: `invalidate-by-${request.reason}`,
848
997
  request: request.request,
@@ -855,9 +1004,9 @@ export class SequencerPublisher {
855
1004
  result.receipt.status === 'success' &&
856
1005
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
857
1006
  if (!success) {
858
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1007
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
859
1008
  } else {
860
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1009
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
861
1010
  }
862
1011
  return !!success;
863
1012
  },
@@ -883,12 +1032,14 @@ export class SequencerPublisher {
883
1032
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
884
1033
 
885
1034
  let gasUsed: bigint;
1035
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
886
1036
  try {
887
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1037
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
888
1038
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
889
1039
  } catch (err) {
890
- const viemError = formatViemError(err);
1040
+ const viemError = formatViemError(err, simulateAbi);
891
1041
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1042
+
892
1043
  return false;
893
1044
  }
894
1045
 
@@ -896,10 +1047,14 @@ export class SequencerPublisher {
896
1047
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
897
1048
  logData.gasLimit = gasLimit;
898
1049
 
1050
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1051
+ // when the tx is sent and a revert is diagnosed via simulation.
1052
+ const requestWithAbi = { ...request, abi: simulateAbi };
1053
+
899
1054
  this.log.debug(`Enqueuing ${action}`, logData);
900
1055
  this.addRequest({
901
1056
  action,
902
- request,
1057
+ request: requestWithAbi,
903
1058
  gasConfig: { gasLimit },
904
1059
  lastValidL2Slot: slotNumber,
905
1060
  checkSuccess: (_req, result) => {
@@ -936,7 +1091,7 @@ export class SequencerPublisher {
936
1091
  private async prepareProposeTx(
937
1092
  encodedData: L1ProcessArgs,
938
1093
  timestamp: bigint,
939
- options: { forcePendingBlockNumber?: number },
1094
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
940
1095
  ) {
941
1096
  const kzg = Blob.getViemKzgInstance();
942
1097
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -981,8 +1136,7 @@ export class SequencerPublisher {
981
1136
  header: encodedData.header.toViem(),
982
1137
  archive: toHex(encodedData.archive),
983
1138
  oracleInput: {
984
- // We are currently not modifying these. See #9963
985
- feeAssetPriceModifier: 0n,
1139
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
986
1140
  },
987
1141
  },
988
1142
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1008,7 +1162,7 @@ export class SequencerPublisher {
1008
1162
  readonly header: ViemHeader;
1009
1163
  readonly archive: `0x${string}`;
1010
1164
  readonly oracleInput: {
1011
- readonly feeAssetPriceModifier: 0n;
1165
+ readonly feeAssetPriceModifier: bigint;
1012
1166
  };
1013
1167
  },
1014
1168
  ViemCommitteeAttestations,
@@ -1017,7 +1171,7 @@ export class SequencerPublisher {
1017
1171
  `0x${string}`,
1018
1172
  ],
1019
1173
  timestamp: bigint,
1020
- options: { forcePendingBlockNumber?: number },
1174
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1021
1175
  ) {
1022
1176
  const rollupData = encodeFunctionData({
1023
1177
  abi: RollupAbi,
@@ -1025,10 +1179,10 @@ export class SequencerPublisher {
1025
1179
  args,
1026
1180
  });
1027
1181
 
1028
- // override the pending block number if requested
1029
- const forcePendingBlockNumberStateDiff = (
1030
- options.forcePendingBlockNumber !== undefined
1031
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber)
1182
+ // override the pending checkpoint number if requested
1183
+ const forcePendingCheckpointNumberStateDiff = (
1184
+ options.forcePendingCheckpointNumber !== undefined
1185
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1032
1186
  : []
1033
1187
  ).flatMap(override => override.stateDiff ?? []);
1034
1188
 
@@ -1038,7 +1192,7 @@ export class SequencerPublisher {
1038
1192
  // @note we override checkBlob to false since blobs are not part simulate()
1039
1193
  stateDiff: [
1040
1194
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1041
- ...forcePendingBlockNumberStateDiff,
1195
+ ...forcePendingCheckpointNumberStateDiff,
1042
1196
  ],
1043
1197
  },
1044
1198
  ];
@@ -1055,20 +1209,20 @@ export class SequencerPublisher {
1055
1209
  {
1056
1210
  to: this.rollupContract.address,
1057
1211
  data: rollupData,
1058
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1212
+ gas: MAX_L1_TX_LIMIT,
1059
1213
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1060
1214
  },
1061
1215
  {
1062
1216
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1063
1217
  time: timestamp + 1n,
1064
1218
  // @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,
1219
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1066
1220
  },
1067
1221
  stateOverrides,
1068
1222
  RollupAbi,
1069
1223
  {
1070
1224
  // @note fallback gas estimate to use if the node doesn't support simulation API
1071
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1225
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1072
1226
  },
1073
1227
  )
1074
1228
  .catch(err => {
@@ -1078,7 +1232,7 @@ export class SequencerPublisher {
1078
1232
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1079
1233
  // Return a minimal simulation result with the fallback gas estimate
1080
1234
  return {
1081
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1235
+ gasUsed: MAX_L1_TX_LIMIT,
1082
1236
  logs: [],
1083
1237
  };
1084
1238
  }
@@ -1090,11 +1244,12 @@ export class SequencerPublisher {
1090
1244
  }
1091
1245
 
1092
1246
  private async addProposeTx(
1093
- block: L2Block,
1247
+ checkpoint: Checkpoint,
1094
1248
  encodedData: L1ProcessArgs,
1095
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
1249
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1096
1250
  timestamp: bigint,
1097
1251
  ): Promise<void> {
1252
+ const slot = checkpoint.header.slotNumber;
1098
1253
  const timer = new Timer();
1099
1254
  const kzg = Blob.getViemKzgInstance();
1100
1255
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
@@ -1109,11 +1264,13 @@ export class SequencerPublisher {
1109
1264
  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
1265
  );
1111
1266
 
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
- });
1267
+ // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1268
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
1269
+ void Promise.resolve().then(() =>
1270
+ this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
1271
+ this.log.error('Failed to send blobs to blob client');
1272
+ }),
1273
+ );
1117
1274
 
1118
1275
  return this.addRequest({
1119
1276
  action: 'propose',
@@ -1121,7 +1278,7 @@ export class SequencerPublisher {
1121
1278
  to: this.rollupContract.address,
1122
1279
  data: rollupData,
1123
1280
  },
1124
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1281
+ lastValidL2Slot: checkpoint.header.slotNumber,
1125
1282
  gasConfig: { ...opts, gasLimit },
1126
1283
  blobConfig: {
1127
1284
  blobs: encodedData.blobs.map(b => b.data),
@@ -1136,11 +1293,12 @@ export class SequencerPublisher {
1136
1293
  receipt &&
1137
1294
  receipt.status === 'success' &&
1138
1295
  tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1296
+
1139
1297
  if (success) {
1140
1298
  const endBlock = receipt.blockNumber;
1141
1299
  const inclusionBlocks = Number(endBlock - startBlock);
1142
1300
  const { calldataGas, calldataSize, sender } = stats!;
1143
- const publishStats: L1PublishBlockStats = {
1301
+ const publishStats: L1PublishCheckpointStats = {
1144
1302
  gasPrice: receipt.effectiveGasPrice,
1145
1303
  gasUsed: receipt.gasUsed,
1146
1304
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1149,23 +1307,26 @@ export class SequencerPublisher {
1149
1307
  calldataGas,
1150
1308
  calldataSize,
1151
1309
  sender,
1152
- ...block.getStats(),
1310
+ ...checkpoint.getStats(),
1153
1311
  eventName: 'rollup-published-to-l1',
1154
1312
  blobCount: encodedData.blobs.length,
1155
1313
  inclusionBlocks,
1156
1314
  };
1157
- this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats(), ...receipt });
1315
+ this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
1316
+ ...stats,
1317
+ ...checkpoint.getStats(),
1318
+ ...pick(receipt, 'transactionHash', 'blockHash'),
1319
+ });
1158
1320
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1159
1321
 
1160
1322
  return true;
1161
1323
  } else {
1162
1324
  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
- });
1325
+ this.log.error(
1326
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1327
+ undefined,
1328
+ { ...checkpoint.getStats(), ...receipt },
1329
+ );
1169
1330
  return false;
1170
1331
  }
1171
1332
  },