@aztec/sequencer-client 0.0.1-fake-ceab37513c → 0.0.2-commit.217f559981

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