@aztec/sequencer-client 4.0.0-nightly.20250907 → 4.0.0-nightly.20260108

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