@aztec/sequencer-client 0.0.1-commit.9b94fc1 → 0.0.1-commit.c7c42ec

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 (95) hide show
  1. package/dest/client/sequencer-client.d.ts +10 -9
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +32 -25
  4. package/dest/config.d.ts +12 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +68 -30
  7. package/dest/global_variable_builder/global_builder.d.ts +19 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +39 -29
  10. package/dest/index.d.ts +2 -2
  11. package/dest/index.d.ts.map +1 -1
  12. package/dest/index.js +1 -1
  13. package/dest/publisher/config.d.ts +7 -4
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +9 -3
  16. package/dest/publisher/sequencer-publisher-factory.d.ts +5 -4
  17. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  18. package/dest/publisher/sequencer-publisher-factory.js +1 -1
  19. package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -3
  20. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher.d.ts +40 -33
  22. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher.js +133 -74
  24. package/dest/sequencer/block_builder.d.ts +4 -5
  25. package/dest/sequencer/block_builder.d.ts.map +1 -1
  26. package/dest/sequencer/block_builder.js +8 -13
  27. package/dest/sequencer/checkpoint_builder.d.ts +63 -0
  28. package/dest/sequencer/checkpoint_builder.d.ts.map +1 -0
  29. package/dest/sequencer/checkpoint_builder.js +131 -0
  30. package/dest/sequencer/checkpoint_proposal_job.d.ts +74 -0
  31. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  32. package/dest/sequencer/checkpoint_proposal_job.js +642 -0
  33. package/dest/sequencer/checkpoint_voter.d.ts +34 -0
  34. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
  35. package/dest/sequencer/checkpoint_voter.js +85 -0
  36. package/dest/sequencer/config.d.ts +3 -2
  37. package/dest/sequencer/config.d.ts.map +1 -1
  38. package/dest/sequencer/events.d.ts +46 -0
  39. package/dest/sequencer/events.d.ts.map +1 -0
  40. package/dest/sequencer/events.js +1 -0
  41. package/dest/sequencer/index.d.ts +5 -1
  42. package/dest/sequencer/index.d.ts.map +1 -1
  43. package/dest/sequencer/index.js +4 -0
  44. package/dest/sequencer/metrics.d.ts +22 -2
  45. package/dest/sequencer/metrics.d.ts.map +1 -1
  46. package/dest/sequencer/metrics.js +154 -0
  47. package/dest/sequencer/sequencer.d.ts +93 -127
  48. package/dest/sequencer/sequencer.d.ts.map +1 -1
  49. package/dest/sequencer/sequencer.js +223 -578
  50. package/dest/sequencer/timetable.d.ts +54 -14
  51. package/dest/sequencer/timetable.d.ts.map +1 -1
  52. package/dest/sequencer/timetable.js +148 -59
  53. package/dest/sequencer/types.d.ts +3 -0
  54. package/dest/sequencer/types.d.ts.map +1 -0
  55. package/dest/sequencer/types.js +1 -0
  56. package/dest/sequencer/utils.d.ts +14 -8
  57. package/dest/sequencer/utils.d.ts.map +1 -1
  58. package/dest/sequencer/utils.js +7 -4
  59. package/dest/test/index.d.ts +4 -2
  60. package/dest/test/index.d.ts.map +1 -1
  61. package/dest/test/mock_checkpoint_builder.d.ts +83 -0
  62. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
  63. package/dest/test/mock_checkpoint_builder.js +179 -0
  64. package/dest/test/utils.d.ts +49 -0
  65. package/dest/test/utils.d.ts.map +1 -0
  66. package/dest/test/utils.js +94 -0
  67. package/dest/tx_validator/tx_validator_factory.d.ts +3 -2
  68. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  69. package/dest/tx_validator/tx_validator_factory.js +1 -1
  70. package/package.json +29 -29
  71. package/src/client/sequencer-client.ts +28 -38
  72. package/src/config.ts +73 -34
  73. package/src/global_variable_builder/global_builder.ts +54 -50
  74. package/src/index.ts +2 -0
  75. package/src/publisher/config.ts +12 -9
  76. package/src/publisher/sequencer-publisher-factory.ts +5 -4
  77. package/src/publisher/sequencer-publisher-metrics.ts +2 -2
  78. package/src/publisher/sequencer-publisher.ts +214 -102
  79. package/src/sequencer/README.md +531 -0
  80. package/src/sequencer/block_builder.ts +11 -16
  81. package/src/sequencer/checkpoint_builder.ts +217 -0
  82. package/src/sequencer/checkpoint_proposal_job.ts +706 -0
  83. package/src/sequencer/checkpoint_voter.ts +105 -0
  84. package/src/sequencer/config.ts +2 -1
  85. package/src/sequencer/events.ts +27 -0
  86. package/src/sequencer/index.ts +4 -0
  87. package/src/sequencer/metrics.ts +203 -1
  88. package/src/sequencer/sequencer.ts +330 -788
  89. package/src/sequencer/timetable.ts +173 -79
  90. package/src/sequencer/types.ts +6 -0
  91. package/src/sequencer/utils.ts +18 -9
  92. package/src/test/index.ts +3 -1
  93. package/src/test/mock_checkpoint_builder.ts +247 -0
  94. package/src/test/utils.ts +137 -0
  95. package/src/tx_validator/tx_validator_factory.ts +3 -2
@@ -1,43 +1,45 @@
1
- import { L2Block } from '@aztec/aztec.js/block';
1
+ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
- import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
4
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
+ import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
- FormattedViemError,
8
7
  type GovernanceProposerContract,
9
8
  type IEmpireBase,
10
- type L1BlobInputs,
11
- type L1ContractsConfig,
12
- type L1TxConfig,
13
- type L1TxRequest,
14
9
  MULTI_CALL_3_ADDRESS,
15
10
  Multicall3,
16
11
  RollupContract,
17
12
  type TallySlashingProposerContract,
18
- type TransactionStats,
19
13
  type ViemCommitteeAttestations,
20
14
  type ViemHeader,
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,
21
22
  WEI_CONST,
22
- formatViemError,
23
- tryExtractEvent,
24
- } from '@aztec/ethereum';
23
+ } from '@aztec/ethereum/l1-tx-utils';
25
24
  import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
+ import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
26
26
  import { sumBigint } from '@aztec/foundation/bigint';
27
27
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
- import { SlotNumber } from '@aztec/foundation/branded-types';
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
32
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
31
- import type { Fr } from '@aztec/foundation/fields';
32
33
  import { type Logger, createLogger } from '@aztec/foundation/log';
33
34
  import { bufferToHex } from '@aztec/foundation/string';
34
35
  import { DateProvider, Timer } from '@aztec/foundation/timer';
35
36
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
36
37
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
37
- import { CommitteeAttestation, CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
38
+ import { CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
39
+ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
38
40
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
39
41
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
40
- import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
42
+ import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
41
43
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
42
44
 
43
45
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
@@ -82,8 +84,8 @@ export type InvalidateBlockRequest = {
82
84
  request: L1TxRequest;
83
85
  reason: 'invalid-attestation' | 'insufficient-attestations';
84
86
  gasUsed: bigint;
85
- blockNumber: number;
86
- forcePendingBlockNumber: number;
87
+ blockNumber: BlockNumber;
88
+ forcePendingBlockNumber: BlockNumber;
87
89
  };
88
90
 
89
91
  interface RequestWithExpiry {
@@ -108,13 +110,18 @@ export class SequencerPublisher {
108
110
 
109
111
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
110
112
 
113
+ private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
114
+
111
115
  protected log: Logger;
112
116
  protected ethereumSlotDuration: bigint;
113
117
 
114
- private blobSinkClient: BlobSinkClientInterface;
118
+ private blobClient: BlobClientInterface;
115
119
 
116
120
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
117
121
  private proposerAddressForSimulation?: EthAddress;
122
+
123
+ /** L1 fee analyzer for fisherman mode */
124
+ private l1FeeAnalyzer?: L1FeeAnalyzer;
118
125
  // @note - with blobs, the below estimate seems too large.
119
126
  // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
120
127
  // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
@@ -138,7 +145,7 @@ export class SequencerPublisher {
138
145
  private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
139
146
  deps: {
140
147
  telemetry?: TelemetryClient;
141
- blobSinkClient?: BlobSinkClientInterface;
148
+ blobClient: BlobClientInterface;
142
149
  l1TxUtils: L1TxUtilsWithBlobs;
143
150
  rollupContract: RollupContract;
144
151
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -156,8 +163,7 @@ export class SequencerPublisher {
156
163
  this.epochCache = deps.epochCache;
157
164
  this.lastActions = deps.lastActions;
158
165
 
159
- this.blobSinkClient =
160
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
166
+ this.blobClient = deps.blobClient;
161
167
 
162
168
  const telemetry = deps.telemetry ?? getTelemetryClient();
163
169
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
@@ -174,6 +180,15 @@ export class SequencerPublisher {
174
180
  this.slashingProposerContract = newSlashingProposer;
175
181
  });
176
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
+ }
177
192
  }
178
193
 
179
194
  public getRollupContract(): RollupContract {
@@ -184,6 +199,13 @@ export class SequencerPublisher {
184
199
  return this.l1TxUtils.getSenderAddress();
185
200
  }
186
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
+
187
209
  /**
188
210
  * Sets the proposer address to use for simulations in fisherman mode.
189
211
  * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
@@ -211,6 +233,62 @@ export class SequencerPublisher {
211
233
  }
212
234
  }
213
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
+
214
292
  /**
215
293
  * Sends all requests that are still valid.
216
294
  * @returns one of:
@@ -221,7 +299,7 @@ export class SequencerPublisher {
221
299
  public async sendRequests() {
222
300
  const requestsToProcess = [...this.requests];
223
301
  this.requests = [];
224
- if (this.interrupted) {
302
+ if (this.interrupted || requestsToProcess.length === 0) {
225
303
  return undefined;
226
304
  }
227
305
  const currentL2Slot = this.getCurrentL2Slot();
@@ -335,14 +413,17 @@ export class SequencerPublisher {
335
413
  public canProposeAtNextEthBlock(
336
414
  tipArchive: Fr,
337
415
  msgSender: EthAddress,
338
- opts: { forcePendingBlockNumber?: number } = {},
416
+ opts: { forcePendingBlockNumber?: BlockNumber } = {},
339
417
  ) {
340
418
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
341
419
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
342
420
 
343
421
  return this.rollupContract
344
422
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
345
- forcePendingCheckpointNumber: opts.forcePendingBlockNumber,
423
+ forcePendingCheckpointNumber:
424
+ opts.forcePendingBlockNumber !== undefined
425
+ ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
426
+ : undefined,
346
427
  })
347
428
  .catch(err => {
348
429
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -361,7 +442,10 @@ export class SequencerPublisher {
361
442
  * It will throw if the block header is invalid.
362
443
  * @param header - The block header to validate
363
444
  */
364
- public async validateBlockHeader(header: CheckpointHeader, opts?: { forcePendingBlockNumber: number | undefined }) {
445
+ public async validateBlockHeader(
446
+ header: CheckpointHeader,
447
+ opts?: { forcePendingBlockNumber: BlockNumber | undefined },
448
+ ) {
365
449
  const flags = { ignoreDA: true, ignoreSignatures: true };
366
450
 
367
451
  const args = [
@@ -375,7 +459,13 @@ export class SequencerPublisher {
375
459
  ] as const;
376
460
 
377
461
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
378
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingBlockNumber);
462
+ const optsForcePendingCheckpointNumber =
463
+ opts?.forcePendingBlockNumber !== undefined
464
+ ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
465
+ : undefined;
466
+ const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
467
+ optsForcePendingCheckpointNumber,
468
+ );
379
469
  let balance = 0n;
380
470
  if (this.config.fishermanMode) {
381
471
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -432,7 +522,7 @@ export class SequencerPublisher {
432
522
  const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
433
523
  this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
434
524
 
435
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
525
+ return { request, gasUsed, blockNumber, forcePendingBlockNumber: BlockNumber(blockNumber - 1), reason };
436
526
  } catch (err) {
437
527
  const viemError = formatViemError(err);
438
528
 
@@ -480,14 +570,14 @@ export class SequencerPublisher {
480
570
 
481
571
  if (reason === 'invalid-attestation') {
482
572
  return this.rollupContract.buildInvalidateBadAttestationRequest(
483
- block.blockNumber,
573
+ CheckpointNumber.fromBlockNumber(block.blockNumber),
484
574
  attestationsAndSigners,
485
575
  committee,
486
576
  validationResult.invalidIndex,
487
577
  );
488
578
  } else if (reason === 'insufficient-attestations') {
489
579
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
490
- block.blockNumber,
580
+ CheckpointNumber.fromBlockNumber(block.blockNumber),
491
581
  attestationsAndSigners,
492
582
  committee,
493
583
  );
@@ -497,45 +587,38 @@ export class SequencerPublisher {
497
587
  }
498
588
  }
499
589
 
500
- /**
501
- * @notice Will simulate `propose` to make sure that the block is valid for submission
502
- *
503
- * @dev Throws if unable to propose
504
- *
505
- * @param block - The block to propose
506
- * @param attestationData - The block's attestation data
507
- *
508
- */
509
- public async validateBlockForSubmission(
510
- block: L2Block,
590
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */
591
+ public async validateCheckpointForSubmission(
592
+ checkpoint: Checkpoint,
511
593
  attestationsAndSigners: CommitteeAttestationsAndSigners,
512
594
  attestationsAndSignersSignature: Signature,
513
- options: { forcePendingBlockNumber?: number },
595
+ options: { forcePendingBlockNumber?: BlockNumber }, // TODO(palla/mbps): Should this be forcePendingCheckpointNumber?
514
596
  ): Promise<bigint> {
515
597
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
516
598
 
599
+ // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
517
600
  // If we have no attestations, we still need to provide the empty attestations
518
601
  // so that the committee is recalculated correctly
519
- const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
520
- if (ignoreSignatures) {
521
- const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
522
- if (!committee) {
523
- this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
524
- throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
525
- }
526
- attestationsAndSigners.attestations = committee.map(committeeMember =>
527
- CommitteeAttestation.fromAddress(committeeMember),
528
- );
529
- }
530
-
531
- const blobFields = block.getCheckpointBlobFields();
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();
532
615
  const blobs = getBlobsPerL1Block(blobFields);
533
616
  const blobInput = getPrefixedEthBlobCommitments(blobs);
534
617
 
535
618
  const args = [
536
619
  {
537
- header: block.getCheckpointHeader().toViem(),
538
- archive: toHex(block.archive.root.toBuffer()),
620
+ header: checkpoint.header.toViem(),
621
+ archive: toHex(checkpoint.archive.root.toBuffer()),
539
622
  oracleInput: {
540
623
  feeAssetPriceModifier: 0n,
541
624
  },
@@ -573,10 +656,19 @@ export class SequencerPublisher {
573
656
  const round = await base.computeRound(slotNumber);
574
657
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
575
658
 
659
+ if (roundInfo.quorumReached) {
660
+ return false;
661
+ }
662
+
576
663
  if (roundInfo.lastSignalSlot >= slotNumber) {
577
664
  return false;
578
665
  }
579
666
 
667
+ if (await this.isPayloadEmpty(payload)) {
668
+ this.log.warn(`Skipping vote cast for payload with empty code`);
669
+ return false;
670
+ }
671
+
580
672
  const cachedLastVote = this.lastActions[signalType];
581
673
  this.lastActions[signalType] = slotNumber;
582
674
  const action = signalType;
@@ -619,14 +711,14 @@ export class SequencerPublisher {
619
711
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
620
712
  if (!success) {
621
713
  this.log.error(
622
- `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`,
623
715
  logData,
624
716
  );
625
717
  this.lastActions[signalType] = cachedLastVote;
626
718
  return false;
627
719
  } else {
628
720
  this.log.info(
629
- `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`,
630
722
  logData,
631
723
  );
632
724
  return true;
@@ -636,6 +728,17 @@ export class SequencerPublisher {
636
728
  return true;
637
729
  }
638
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
+
639
742
  /**
640
743
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
641
744
  * @param slotNumber - The slot number to cast a signal for.
@@ -783,27 +886,21 @@ export class SequencerPublisher {
783
886
  return true;
784
887
  }
785
888
 
786
- /**
787
- * Proposes a L2 block on L1.
788
- *
789
- * @param block - L2 block to propose.
790
- * @returns True if the tx has been enqueued, throws otherwise. See #9315
791
- */
792
- public async enqueueProposeL2Block(
793
- block: L2Block,
889
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
890
+ public async enqueueProposeCheckpoint(
891
+ checkpoint: Checkpoint,
794
892
  attestationsAndSigners: CommitteeAttestationsAndSigners,
795
893
  attestationsAndSignersSignature: Signature,
796
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
797
- ): Promise<boolean> {
798
- const checkpointHeader = block.getCheckpointHeader();
894
+ opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
895
+ ): Promise<void> {
896
+ const checkpointHeader = checkpoint.header;
799
897
 
800
- const blobFields = block.getCheckpointBlobFields();
898
+ const blobFields = checkpoint.toBlobFields();
801
899
  const blobs = getBlobsPerL1Block(blobFields);
802
900
 
803
901
  const proposeTxArgs = {
804
902
  header: checkpointHeader,
805
- archive: block.archive.root.toBuffer(),
806
- body: block.body.toBuffer(),
903
+ archive: checkpoint.archive.root.toBuffer(),
807
904
  blobs,
808
905
  attestationsAndSigners,
809
906
  attestationsAndSignersSignature,
@@ -817,19 +914,23 @@ export class SequencerPublisher {
817
914
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
818
915
  // make time consistency checks break.
819
916
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
820
- ts = await this.validateBlockForSubmission(block, attestationsAndSigners, attestationsAndSignersSignature, opts);
917
+ ts = await this.validateCheckpointForSubmission(
918
+ checkpoint,
919
+ attestationsAndSigners,
920
+ attestationsAndSignersSignature,
921
+ opts,
922
+ );
821
923
  } catch (err: any) {
822
- this.log.error(`Block validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
823
- ...block.getStats(),
824
- slotNumber: block.header.globalVariables.slotNumber,
924
+ this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
925
+ ...checkpoint.getStats(),
926
+ slotNumber: checkpoint.header.slotNumber,
825
927
  forcePendingBlockNumber: opts.forcePendingBlockNumber,
826
928
  });
827
929
  throw err;
828
930
  }
829
931
 
830
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
831
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
832
- return true;
932
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
933
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
833
934
  }
834
935
 
835
936
  public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
@@ -936,7 +1037,7 @@ export class SequencerPublisher {
936
1037
  private async prepareProposeTx(
937
1038
  encodedData: L1ProcessArgs,
938
1039
  timestamp: bigint,
939
- options: { forcePendingBlockNumber?: number },
1040
+ options: { forcePendingBlockNumber?: BlockNumber },
940
1041
  ) {
941
1042
  const kzg = Blob.getViemKzgInstance();
942
1043
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -1017,7 +1118,7 @@ export class SequencerPublisher {
1017
1118
  `0x${string}`,
1018
1119
  ],
1019
1120
  timestamp: bigint,
1020
- options: { forcePendingBlockNumber?: number },
1121
+ options: { forcePendingBlockNumber?: BlockNumber },
1021
1122
  ) {
1022
1123
  const rollupData = encodeFunctionData({
1023
1124
  abi: RollupAbi,
@@ -1025,10 +1126,14 @@ export class SequencerPublisher {
1025
1126
  args,
1026
1127
  });
1027
1128
 
1028
- // override the pending block number if requested
1029
- const forcePendingBlockNumberStateDiff = (
1129
+ // override the pending checkpoint number if requested
1130
+ const optsForcePendingCheckpointNumber =
1030
1131
  options.forcePendingBlockNumber !== undefined
1031
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber)
1132
+ ? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber)
1133
+ : undefined;
1134
+ const forcePendingCheckpointNumberStateDiff = (
1135
+ optsForcePendingCheckpointNumber !== undefined
1136
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber)
1032
1137
  : []
1033
1138
  ).flatMap(override => override.stateDiff ?? []);
1034
1139
 
@@ -1038,7 +1143,7 @@ export class SequencerPublisher {
1038
1143
  // @note we override checkBlob to false since blobs are not part simulate()
1039
1144
  stateDiff: [
1040
1145
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1041
- ...forcePendingBlockNumberStateDiff,
1146
+ ...forcePendingCheckpointNumberStateDiff,
1042
1147
  ],
1043
1148
  },
1044
1149
  ];
@@ -1090,11 +1195,12 @@ export class SequencerPublisher {
1090
1195
  }
1091
1196
 
1092
1197
  private async addProposeTx(
1093
- block: L2Block,
1198
+ checkpoint: Checkpoint,
1094
1199
  encodedData: L1ProcessArgs,
1095
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
1200
+ opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1096
1201
  timestamp: bigint,
1097
1202
  ): Promise<void> {
1203
+ const slot = checkpoint.header.slotNumber;
1098
1204
  const timer = new Timer();
1099
1205
  const kzg = Blob.getViemKzgInstance();
1100
1206
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
@@ -1109,11 +1215,13 @@ export class SequencerPublisher {
1109
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
1110
1216
  );
1111
1217
 
1112
- // Send the blobs to the blob sink preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1113
- // tx fails but it does get mined. We make sure that the blobs are sent to the blob sink regardless of the tx outcome.
1114
- void this.blobSinkClient.sendBlobsToBlobSink(encodedData.blobs).catch(_err => {
1115
- this.log.error('Failed to send blobs to blob sink');
1116
- });
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
+ );
1117
1225
 
1118
1226
  return this.addRequest({
1119
1227
  action: 'propose',
@@ -1121,7 +1229,7 @@ export class SequencerPublisher {
1121
1229
  to: this.rollupContract.address,
1122
1230
  data: rollupData,
1123
1231
  },
1124
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1232
+ lastValidL2Slot: checkpoint.header.slotNumber,
1125
1233
  gasConfig: { ...opts, gasLimit },
1126
1234
  blobConfig: {
1127
1235
  blobs: encodedData.blobs.map(b => b.data),
@@ -1136,11 +1244,12 @@ export class SequencerPublisher {
1136
1244
  receipt &&
1137
1245
  receipt.status === 'success' &&
1138
1246
  tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1247
+
1139
1248
  if (success) {
1140
1249
  const endBlock = receipt.blockNumber;
1141
1250
  const inclusionBlocks = Number(endBlock - startBlock);
1142
1251
  const { calldataGas, calldataSize, sender } = stats!;
1143
- const publishStats: L1PublishBlockStats = {
1252
+ const publishStats: L1PublishCheckpointStats = {
1144
1253
  gasPrice: receipt.effectiveGasPrice,
1145
1254
  gasUsed: receipt.gasUsed,
1146
1255
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1149,23 +1258,26 @@ export class SequencerPublisher {
1149
1258
  calldataGas,
1150
1259
  calldataSize,
1151
1260
  sender,
1152
- ...block.getStats(),
1261
+ ...checkpoint.getStats(),
1153
1262
  eventName: 'rollup-published-to-l1',
1154
1263
  blobCount: encodedData.blobs.length,
1155
1264
  inclusionBlocks,
1156
1265
  };
1157
- 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
+ });
1158
1271
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1159
1272
 
1160
1273
  return true;
1161
1274
  } else {
1162
1275
  this.metrics.recordFailedTx('process');
1163
- this.log.error(`Rollup process tx failed: ${errorMsg ?? 'no error message'}`, undefined, {
1164
- ...block.getStats(),
1165
- receipt,
1166
- txHash: receipt.transactionHash,
1167
- slotNumber: block.header.globalVariables.slotNumber,
1168
- });
1276
+ this.log.error(
1277
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1278
+ undefined,
1279
+ { ...checkpoint.getStats(), ...receipt },
1280
+ );
1169
1281
  return false;
1170
1282
  }
1171
1283
  },