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

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