@aztec/sequencer-client 0.0.1-commit.b655e406 → 0.0.1-commit.d1f2d6c

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