@aztec/sequencer-client 0.0.1-commit.24de95ac → 0.0.1-commit.2e2504e2

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