@aztec/sequencer-client 0.0.1-fake-c83136db25 → 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 +90 -33
  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 +15 -6
  20. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher-factory.js +14 -3
  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 +89 -67
  26. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  27. package/dest/publisher/sequencer-publisher.js +722 -178
  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 +44 -3
  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 +122 -144
  48. package/dest/sequencer/sequencer.d.ts.map +1 -1
  49. package/dest/sequencer/sequencer.js +740 -525
  50. package/dest/sequencer/timetable.d.ts +51 -14
  51. package/dest/sequencer/timetable.d.ts.map +1 -1
  52. package/dest/sequencer/timetable.js +145 -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 +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 +103 -42
  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 +30 -11
  74. package/src/publisher/sequencer-publisher-metrics.ts +19 -71
  75. package/src/publisher/sequencer-publisher.ts +461 -232
  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 +296 -61
  83. package/src/sequencer/sequencer.ts +494 -717
  84. package/src/sequencer/timetable.ts +175 -80
  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 -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,48 +1,53 @@
1
- import { L2Block } from '@aztec/aztec.js/block';
1
+ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
- import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
4
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
+ import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
- FormattedViemError,
7
+ FeeAssetPriceOracle,
8
8
  type GovernanceProposerContract,
9
9
  type IEmpireBase,
10
- type L1BlobInputs,
11
- type L1ContractsConfig,
12
- type L1TxConfig,
13
- type L1TxRequest,
14
10
  MULTI_CALL_3_ADDRESS,
15
11
  Multicall3,
16
12
  RollupContract,
17
13
  type TallySlashingProposerContract,
18
- type TransactionStats,
19
14
  type ViemCommitteeAttestations,
20
15
  type ViemHeader,
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
44
  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';
45
+ import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
46
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
42
47
 
43
- import { type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
48
+ import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
44
49
 
45
- import type { PublisherConfig, TxSenderConfig } from './config.js';
50
+ import type { SequencerPublisherConfig } from './config.js';
46
51
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
47
52
 
48
53
  /** Arguments to the process method of the rollup contract */
@@ -51,14 +56,14 @@ type L1ProcessArgs = {
51
56
  header: CheckpointHeader;
52
57
  /** A root of the archive tree after the L2 block is applied. */
53
58
  archive: Buffer;
54
- /** State reference after the L2 block is applied. */
55
- stateReference: StateReference;
56
59
  /** L2 block blobs containing all tx effects. */
57
60
  blobs: Blob[];
58
61
  /** Attestations */
59
62
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
63
  /** Attestations and signers signature */
61
64
  attestationsAndSignersSignature: Signature;
65
+ /** The fee asset price modifier in basis points (from oracle) */
66
+ feeAssetPriceModifier: bigint;
62
67
  };
63
68
 
64
69
  export const Actions = [
@@ -80,18 +85,18 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
80
85
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
81
86
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
82
87
 
83
- export type InvalidateBlockRequest = {
88
+ export type InvalidateCheckpointRequest = {
84
89
  request: L1TxRequest;
85
90
  reason: 'invalid-attestation' | 'insufficient-attestations';
86
91
  gasUsed: bigint;
87
- blockNumber: number;
88
- forcePendingBlockNumber: number;
92
+ checkpointNumber: CheckpointNumber;
93
+ forcePendingCheckpointNumber: CheckpointNumber;
89
94
  };
90
95
 
91
96
  interface RequestWithExpiry {
92
97
  action: Action;
93
98
  request: L1TxRequest;
94
- lastValidL2Slot: bigint;
99
+ lastValidL2Slot: SlotNumber;
95
100
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
96
101
  blobConfig?: L1BlobInputs;
97
102
  checkSuccess: (
@@ -108,16 +113,24 @@ export class SequencerPublisher {
108
113
  protected governanceLog = createLogger('sequencer:publisher:governance');
109
114
  protected slashingLog = createLogger('sequencer:publisher:slashing');
110
115
 
111
- 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>();
112
120
 
113
121
  protected log: Logger;
114
122
  protected ethereumSlotDuration: bigint;
115
123
 
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;
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;
121
134
 
122
135
  // A CALL to a cold address is 2700 gas
123
136
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -125,20 +138,23 @@ export class SequencerPublisher {
125
138
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
126
139
  public static VOTE_GAS_GUESS: bigint = 800_000n;
127
140
 
128
- public l1TxUtils: L1TxUtilsWithBlobs;
141
+ public l1TxUtils: L1TxUtils;
129
142
  public rollupContract: RollupContract;
130
143
  public govProposerContract: GovernanceProposerContract;
131
144
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
132
145
  public slashFactoryContract: SlashFactoryContract;
133
146
 
147
+ public readonly tracer: Tracer;
148
+
134
149
  protected requests: RequestWithExpiry[] = [];
135
150
 
136
151
  constructor(
137
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
152
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
153
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
138
154
  deps: {
139
155
  telemetry?: TelemetryClient;
140
- blobSinkClient?: BlobSinkClientInterface;
141
- l1TxUtils: L1TxUtilsWithBlobs;
156
+ blobClient: BlobClientInterface;
157
+ l1TxUtils: L1TxUtils;
142
158
  rollupContract: RollupContract;
143
159
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
144
160
  governanceProposerContract: GovernanceProposerContract;
@@ -146,7 +162,7 @@ export class SequencerPublisher {
146
162
  epochCache: EpochCache;
147
163
  dateProvider: DateProvider;
148
164
  metrics: SequencerPublisherMetrics;
149
- lastActions: Partial<Record<Action, bigint>>;
165
+ lastActions: Partial<Record<Action, SlotNumber>>;
150
166
  log?: Logger;
151
167
  },
152
168
  ) {
@@ -155,11 +171,11 @@ export class SequencerPublisher {
155
171
  this.epochCache = deps.epochCache;
156
172
  this.lastActions = deps.lastActions;
157
173
 
158
- this.blobSinkClient =
159
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
174
+ this.blobClient = deps.blobClient;
160
175
 
161
176
  const telemetry = deps.telemetry ?? getTelemetryClient();
162
177
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
178
+ this.tracer = telemetry.getTracer('SequencerPublisher');
163
179
  this.l1TxUtils = deps.l1TxUtils;
164
180
 
165
181
  this.rollupContract = deps.rollupContract;
@@ -173,24 +189,130 @@ export class SequencerPublisher {
173
189
  this.slashingProposerContract = newSlashingProposer;
174
190
  });
175
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
+ );
176
208
  }
177
209
 
178
210
  public getRollupContract(): RollupContract {
179
211
  return this.rollupContract;
180
212
  }
181
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
+
182
222
  public getSenderAddress() {
183
223
  return this.l1TxUtils.getSenderAddress();
184
224
  }
185
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
+
186
241
  public addRequest(request: RequestWithExpiry) {
187
242
  this.requests.push(request);
188
243
  }
189
244
 
190
- public getCurrentL2Slot(): bigint {
245
+ public getCurrentL2Slot(): SlotNumber {
191
246
  return this.epochCache.getEpochAndSlotNow().slot;
192
247
  }
193
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
+
194
316
  /**
195
317
  * Sends all requests that are still valid.
196
318
  * @returns one of:
@@ -198,10 +320,11 @@ export class SequencerPublisher {
198
320
  * - a receipt and errorMsg if it failed on L1
199
321
  * - undefined if no valid requests are found OR the tx failed to send.
200
322
  */
323
+ @trackSpan('SequencerPublisher.sendRequests')
201
324
  public async sendRequests() {
202
325
  const requestsToProcess = [...this.requests];
203
326
  this.requests = [];
204
- if (this.interrupted) {
327
+ if (this.interrupted || requestsToProcess.length === 0) {
205
328
  return undefined;
206
329
  }
207
330
  const currentL2Slot = this.getCurrentL2Slot();
@@ -244,7 +367,16 @@ export class SequencerPublisher {
244
367
 
245
368
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
246
369
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
247
- 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
+ }
248
380
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
249
381
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
250
382
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -315,13 +447,15 @@ export class SequencerPublisher {
315
447
  public canProposeAtNextEthBlock(
316
448
  tipArchive: Fr,
317
449
  msgSender: EthAddress,
318
- opts: { forcePendingBlockNumber?: number } = {},
450
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
319
451
  ) {
320
452
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
321
453
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
322
454
 
323
455
  return this.rollupContract
324
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration, opts)
456
+ .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
457
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
458
+ })
325
459
  .catch(err => {
326
460
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
327
461
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -339,7 +473,11 @@ export class SequencerPublisher {
339
473
  * It will throw if the block header is invalid.
340
474
  * @param header - The block header to validate
341
475
  */
342
- public async validateBlockHeader(header: CheckpointHeader, opts?: { forcePendingBlockNumber: number | undefined }) {
476
+ @trackSpan('SequencerPublisher.validateBlockHeader')
477
+ public async validateBlockHeader(
478
+ header: CheckpointHeader,
479
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
480
+ ): Promise<void> {
343
481
  const flags = { ignoreDA: true, ignoreSignatures: true };
344
482
 
345
483
  const args = [
@@ -348,15 +486,27 @@ export class SequencerPublisher {
348
486
  [], // no signers
349
487
  Signature.empty().toViemSignature(),
350
488
  `0x${'0'.repeat(64)}`, // 32 empty bytes
351
- header.contentCommitment.blobsHash.toString(),
489
+ header.blobsHash.toString(),
352
490
  flags,
353
491
  ] as const;
354
492
 
355
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
+ });
356
509
 
357
- // use sender balance to simulate
358
- const balance = await this.l1TxUtils.getSenderBalance();
359
- this.log.debug(`Simulating validateHeader with balance: ${balance}`);
360
510
  await this.l1TxUtils.simulate(
361
511
  {
362
512
  to: this.rollupContract.address,
@@ -364,86 +514,101 @@ export class SequencerPublisher {
364
514
  from: MULTI_CALL_3_ADDRESS,
365
515
  },
366
516
  { time: ts + 1n },
367
- [
368
- { address: MULTI_CALL_3_ADDRESS, balance },
369
- ...(await this.rollupContract.makePendingBlockNumberOverride(opts?.forcePendingBlockNumber)),
370
- ],
517
+ stateOverrides,
371
518
  );
372
519
  this.log.debug(`Simulated validateHeader`);
373
520
  }
374
521
 
375
522
  /**
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)
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)
378
525
  */
379
- public async simulateInvalidateBlock(
380
- validationResult: ValidateBlockResult,
381
- ): Promise<InvalidateBlockRequest | undefined> {
526
+ public async simulateInvalidateCheckpoint(
527
+ validationResult: ValidateCheckpointResult,
528
+ ): Promise<InvalidateCheckpointRequest | undefined> {
382
529
  if (validationResult.valid) {
383
530
  return undefined;
384
531
  }
385
532
 
386
- const { reason, block } = validationResult;
387
- const blockNumber = block.blockNumber;
388
- const logData = { ...block, reason };
533
+ const { reason, checkpoint } = validationResult;
534
+ const checkpointNumber = checkpoint.checkpointNumber;
535
+ const logData = { ...checkpoint, reason };
389
536
 
390
- const currentBlockNumber = await this.rollupContract.getBlockNumber();
391
- if (currentBlockNumber < validationResult.block.blockNumber) {
537
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
538
+ if (currentCheckpointNumber < checkpointNumber) {
392
539
  this.log.verbose(
393
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
394
- { currentBlockNumber, ...logData },
540
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
541
+ { currentCheckpointNumber, ...logData },
395
542
  );
396
543
  return undefined;
397
544
  }
398
545
 
399
- const request = this.buildInvalidateBlockRequest(validationResult);
400
- 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 });
401
548
 
402
549
  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 });
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
+ });
405
561
 
406
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
562
+ return {
563
+ request,
564
+ gasUsed,
565
+ checkpointNumber,
566
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
567
+ reason,
568
+ };
407
569
  } catch (err) {
408
570
  const viemError = formatViemError(err);
409
571
 
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')) {
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')) {
413
575
  this.log.verbose(
414
- `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`,
415
577
  { ...logData, request, error: viemError.message },
416
578
  );
417
- const latestPendingBlockNumber = await this.rollupContract.getBlockNumber();
418
- if (latestPendingBlockNumber < blockNumber) {
419
- 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 });
420
582
  return undefined;
421
583
  } else {
422
584
  this.log.error(
423
- `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`,
424
586
  viemError,
425
587
  logData,
426
588
  );
427
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
428
- cause: viemError,
429
- });
589
+ throw new Error(
590
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
591
+ {
592
+ cause: viemError,
593
+ },
594
+ );
430
595
  }
431
596
  }
432
597
 
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 });
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 });
436
601
  }
437
602
  }
438
603
 
439
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
604
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
440
605
  if (validationResult.valid) {
441
- throw new Error('Cannot invalidate a valid block');
606
+ throw new Error('Cannot invalidate a valid checkpoint');
442
607
  }
443
608
 
444
- const { block, committee, reason } = validationResult;
445
- const logData = { ...block, reason };
446
- 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);
447
612
 
448
613
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
449
614
  validationResult.attestations,
@@ -451,14 +616,14 @@ export class SequencerPublisher {
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,48 +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 blobFields = block.getCheckpointBlobFields();
503
- const blobs = getBlobsPerL1Block(blobFields);
645
+ const blobFields = checkpoint.toBlobFields();
646
+ const blobs = await getBlobsPerL1Block(blobFields);
504
647
  const blobInput = getPrefixedEthBlobCommitments(blobs);
505
648
 
506
649
  const args = [
507
650
  {
508
- header: block.getCheckpointHeader().toViem(),
509
- archive: toHex(block.archive.root.toBuffer()),
510
- stateReference: block.header.state.toViem(),
651
+ header: checkpoint.header.toViem(),
652
+ archive: toHex(checkpoint.archive.root.toBuffer()),
511
653
  oracleInput: {
512
- feeAssetPriceModifier: 0n,
654
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
513
655
  },
514
656
  },
515
657
  attestationsAndSigners.getPackedAttestations(),
@@ -523,7 +665,7 @@ export class SequencerPublisher {
523
665
  }
524
666
 
525
667
  private async enqueueCastSignalHelper(
526
- slotNumber: bigint,
668
+ slotNumber: SlotNumber,
527
669
  timestamp: bigint,
528
670
  signalType: GovernanceSignalAction,
529
671
  payload: EthAddress,
@@ -545,10 +687,45 @@ export class SequencerPublisher {
545
687
  const round = await base.computeRound(slotNumber);
546
688
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
547
689
 
690
+ if (roundInfo.quorumReached) {
691
+ return false;
692
+ }
693
+
548
694
  if (roundInfo.lastSignalSlot >= slotNumber) {
549
695
  return false;
550
696
  }
551
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
+
552
729
  const cachedLastVote = this.lastActions[signalType];
553
730
  this.lastActions[signalType] = slotNumber;
554
731
  const action = signalType;
@@ -568,7 +745,7 @@ export class SequencerPublisher {
568
745
  });
569
746
 
570
747
  try {
571
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
748
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
572
749
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
573
750
  } catch (err) {
574
751
  this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
@@ -591,14 +768,14 @@ export class SequencerPublisher {
591
768
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
592
769
  if (!success) {
593
770
  this.log.error(
594
- `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`,
595
772
  logData,
596
773
  );
597
774
  this.lastActions[signalType] = cachedLastVote;
598
775
  return false;
599
776
  } else {
600
777
  this.log.info(
601
- `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`,
602
779
  logData,
603
780
  );
604
781
  return true;
@@ -608,6 +785,17 @@ export class SequencerPublisher {
608
785
  return true;
609
786
  }
610
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
+
611
799
  /**
612
800
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
613
801
  * @param slotNumber - The slot number to cast a signal for.
@@ -616,7 +804,7 @@ export class SequencerPublisher {
616
804
  */
617
805
  public enqueueGovernanceCastSignal(
618
806
  governancePayload: EthAddress,
619
- slotNumber: bigint,
807
+ slotNumber: SlotNumber,
620
808
  timestamp: bigint,
621
809
  signerAddress: EthAddress,
622
810
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
@@ -635,7 +823,7 @@ export class SequencerPublisher {
635
823
  /** Enqueues all slashing actions as returned by the slasher client. */
636
824
  public async enqueueSlashingActions(
637
825
  actions: ProposerSlashAction[],
638
- slotNumber: bigint,
826
+ slotNumber: SlotNumber,
639
827
  timestamp: bigint,
640
828
  signerAddress: EthAddress,
641
829
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
@@ -755,31 +943,25 @@ export class SequencerPublisher {
755
943
  return true;
756
944
  }
757
945
 
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,
946
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
947
+ public async enqueueProposeCheckpoint(
948
+ checkpoint: Checkpoint,
766
949
  attestationsAndSigners: CommitteeAttestationsAndSigners,
767
950
  attestationsAndSignersSignature: Signature,
768
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
769
- ): Promise<boolean> {
770
- const checkpointHeader = block.getCheckpointHeader();
951
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
952
+ ): Promise<void> {
953
+ const checkpointHeader = checkpoint.header;
771
954
 
772
- const blobFields = block.getCheckpointBlobFields();
773
- const blobs = getBlobsPerL1Block(blobFields);
955
+ const blobFields = checkpoint.toBlobFields();
956
+ const blobs = await getBlobsPerL1Block(blobFields);
774
957
 
775
- const proposeTxArgs = {
958
+ const proposeTxArgs: L1ProcessArgs = {
776
959
  header: checkpointHeader,
777
- archive: block.archive.root.toBuffer(),
778
- stateReference: block.header.state,
779
- body: block.body.toBuffer(),
960
+ archive: checkpoint.archive.root.toBuffer(),
780
961
  blobs,
781
962
  attestationsAndSigners,
782
963
  attestationsAndSignersSignature,
964
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
783
965
  };
784
966
 
785
967
  let ts: bigint;
@@ -790,22 +972,29 @@ export class SequencerPublisher {
790
972
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
791
973
  // make time consistency checks break.
792
974
  // 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);
975
+ ts = await this.validateCheckpointForSubmission(
976
+ checkpoint,
977
+ attestationsAndSigners,
978
+ attestationsAndSignersSignature,
979
+ opts,
980
+ );
794
981
  } 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,
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,
799
986
  });
800
987
  throw err;
801
988
  }
802
989
 
803
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
804
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
805
- return true;
990
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
991
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
806
992
  }
807
993
 
808
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
994
+ public enqueueInvalidateCheckpoint(
995
+ request: InvalidateCheckpointRequest | undefined,
996
+ opts: { txTimeoutAt?: Date } = {},
997
+ ) {
809
998
  if (!request) {
810
999
  return;
811
1000
  }
@@ -813,24 +1002,24 @@ export class SequencerPublisher {
813
1002
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
814
1003
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
815
1004
 
816
- const { gasUsed, blockNumber } = request;
817
- const logData = { gasUsed, blockNumber, gasLimit, opts };
818
- 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);
819
1008
  this.addRequest({
820
1009
  action: `invalidate-by-${request.reason}`,
821
1010
  request: request.request,
822
1011
  gasConfig: { gasLimit, txTimeoutAt: opts.txTimeoutAt },
823
- lastValidL2Slot: this.getCurrentL2Slot() + 2n,
1012
+ lastValidL2Slot: SlotNumber(this.getCurrentL2Slot() + 2),
824
1013
  checkSuccess: (_req, result) => {
825
1014
  const success =
826
1015
  result &&
827
1016
  result.receipt &&
828
1017
  result.receipt.status === 'success' &&
829
- tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'BlockInvalidated');
1018
+ tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
830
1019
  if (!success) {
831
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1020
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
832
1021
  } else {
833
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1022
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
834
1023
  }
835
1024
  return !!success;
836
1025
  },
@@ -841,7 +1030,7 @@ export class SequencerPublisher {
841
1030
  action: Action,
842
1031
  request: L1TxRequest,
843
1032
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
844
- slotNumber: bigint,
1033
+ slotNumber: SlotNumber,
845
1034
  timestamp: bigint,
846
1035
  ) {
847
1036
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
@@ -856,12 +1045,14 @@ export class SequencerPublisher {
856
1045
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
857
1046
 
858
1047
  let gasUsed: bigint;
1048
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
859
1049
  try {
860
- ({ 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
861
1051
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
862
1052
  } catch (err) {
863
- const viemError = formatViemError(err);
1053
+ const viemError = formatViemError(err, simulateAbi);
864
1054
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1055
+
865
1056
  return false;
866
1057
  }
867
1058
 
@@ -869,10 +1060,14 @@ export class SequencerPublisher {
869
1060
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
870
1061
  logData.gasLimit = gasLimit;
871
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
+
872
1067
  this.log.debug(`Enqueuing ${action}`, logData);
873
1068
  this.addRequest({
874
1069
  action,
875
- request,
1070
+ request: requestWithAbi,
876
1071
  gasConfig: { gasLimit },
877
1072
  lastValidL2Slot: slotNumber,
878
1073
  checkSuccess: (_req, result) => {
@@ -909,44 +1104,52 @@ export class SequencerPublisher {
909
1104
  private async prepareProposeTx(
910
1105
  encodedData: L1ProcessArgs,
911
1106
  timestamp: bigint,
912
- options: { forcePendingBlockNumber?: number },
1107
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
913
1108
  ) {
914
1109
  const kzg = Blob.getViemKzgInstance();
915
1110
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
916
1111
  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
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
+ }
940
1145
  const signers = encodedData.attestationsAndSigners.getSigners().map(signer => signer.toString());
941
1146
 
942
1147
  const args = [
943
1148
  {
944
1149
  header: encodedData.header.toViem(),
945
1150
  archive: toHex(encodedData.archive),
946
- stateReference: encodedData.stateReference.toViem(),
947
1151
  oracleInput: {
948
- // We are currently not modifying these. See #9963
949
- feeAssetPriceModifier: 0n,
1152
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
950
1153
  },
951
1154
  },
952
1155
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -971,9 +1174,8 @@ export class SequencerPublisher {
971
1174
  {
972
1175
  readonly header: ViemHeader;
973
1176
  readonly archive: `0x${string}`;
974
- readonly stateReference: ViemStateReference;
975
1177
  readonly oracleInput: {
976
- readonly feeAssetPriceModifier: 0n;
1178
+ readonly feeAssetPriceModifier: bigint;
977
1179
  };
978
1180
  },
979
1181
  ViemCommitteeAttestations,
@@ -982,7 +1184,7 @@ export class SequencerPublisher {
982
1184
  `0x${string}`,
983
1185
  ],
984
1186
  timestamp: bigint,
985
- options: { forcePendingBlockNumber?: number },
1187
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
986
1188
  ) {
987
1189
  const rollupData = encodeFunctionData({
988
1190
  abi: RollupAbi,
@@ -990,44 +1192,64 @@ export class SequencerPublisher {
990
1192
  args,
991
1193
  });
992
1194
 
993
- // override the pending block number if requested
994
- const forcePendingBlockNumberStateDiff = (
995
- options.forcePendingBlockNumber !== undefined
996
- ? 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)
997
1199
  : []
998
1200
  ).flatMap(override => override.stateDiff ?? []);
999
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
+
1000
1220
  const simulationResult = await this.l1TxUtils
1001
1221
  .simulate(
1002
1222
  {
1003
1223
  to: this.rollupContract.address,
1004
1224
  data: rollupData,
1005
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1225
+ gas: MAX_L1_TX_LIMIT,
1226
+ ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1006
1227
  },
1007
1228
  {
1008
1229
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1009
1230
  time: timestamp + 1n,
1010
1231
  // @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,
1232
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1012
1233
  },
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
- ],
1234
+ stateOverrides,
1023
1235
  RollupAbi,
1024
1236
  {
1025
1237
  // @note fallback gas estimate to use if the node doesn't support simulation API
1026
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1238
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1027
1239
  },
1028
1240
  )
1029
1241
  .catch(err => {
1030
- 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);
1031
1253
  throw err;
1032
1254
  });
1033
1255
 
@@ -1035,11 +1257,12 @@ export class SequencerPublisher {
1035
1257
  }
1036
1258
 
1037
1259
  private async addProposeTx(
1038
- block: L2Block,
1260
+ checkpoint: Checkpoint,
1039
1261
  encodedData: L1ProcessArgs,
1040
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
1262
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1041
1263
  timestamp: bigint,
1042
1264
  ): Promise<void> {
1265
+ const slot = checkpoint.header.slotNumber;
1043
1266
  const timer = new Timer();
1044
1267
  const kzg = Blob.getViemKzgInstance();
1045
1268
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
@@ -1054,11 +1277,13 @@ export class SequencerPublisher {
1054
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
1055
1278
  );
1056
1279
 
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
- });
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
+ );
1062
1287
 
1063
1288
  return this.addRequest({
1064
1289
  action: 'propose',
@@ -1066,7 +1291,7 @@ export class SequencerPublisher {
1066
1291
  to: this.rollupContract.address,
1067
1292
  data: rollupData,
1068
1293
  },
1069
- lastValidL2Slot: block.header.globalVariables.slotNumber.toBigInt(),
1294
+ lastValidL2Slot: checkpoint.header.slotNumber,
1070
1295
  gasConfig: { ...opts, gasLimit },
1071
1296
  blobConfig: {
1072
1297
  blobs: encodedData.blobs.map(b => b.data),
@@ -1080,12 +1305,13 @@ export class SequencerPublisher {
1080
1305
  const success =
1081
1306
  receipt &&
1082
1307
  receipt.status === 'success' &&
1083
- tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'L2BlockProposed');
1308
+ tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1309
+
1084
1310
  if (success) {
1085
1311
  const endBlock = receipt.blockNumber;
1086
1312
  const inclusionBlocks = Number(endBlock - startBlock);
1087
1313
  const { calldataGas, calldataSize, sender } = stats!;
1088
- const publishStats: L1PublishBlockStats = {
1314
+ const publishStats: L1PublishCheckpointStats = {
1089
1315
  gasPrice: receipt.effectiveGasPrice,
1090
1316
  gasUsed: receipt.gasUsed,
1091
1317
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1094,23 +1320,26 @@ export class SequencerPublisher {
1094
1320
  calldataGas,
1095
1321
  calldataSize,
1096
1322
  sender,
1097
- ...block.getStats(),
1323
+ ...checkpoint.getStats(),
1098
1324
  eventName: 'rollup-published-to-l1',
1099
1325
  blobCount: encodedData.blobs.length,
1100
1326
  inclusionBlocks,
1101
1327
  };
1102
- 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
+ });
1103
1333
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1104
1334
 
1105
1335
  return true;
1106
1336
  } else {
1107
1337
  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
- });
1338
+ this.log.error(
1339
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1340
+ undefined,
1341
+ { ...checkpoint.getStats(), ...receipt },
1342
+ );
1114
1343
  return false;
1115
1344
  }
1116
1345
  },