@aztec/sequencer-client 0.0.1-commit.fcb71a6 → 0.0.1-commit.ff7989d6c

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 (84) hide show
  1. package/dest/client/sequencer-client.d.ts +14 -10
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +15 -4
  4. package/dest/config.d.ts +3 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +22 -12
  7. package/dest/global_variable_builder/global_builder.d.ts +5 -7
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +13 -13
  10. package/dest/index.d.ts +2 -3
  11. package/dest/index.d.ts.map +1 -1
  12. package/dest/index.js +1 -2
  13. package/dest/publisher/config.d.ts +31 -17
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +101 -42
  16. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  17. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  18. package/dest/publisher/sequencer-publisher-factory.js +13 -2
  19. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  20. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher-metrics.js +23 -86
  22. package/dest/publisher/sequencer-publisher.d.ts +32 -23
  23. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher.js +522 -88
  25. package/dest/sequencer/checkpoint_proposal_job.d.ts +40 -12
  26. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  27. package/dest/sequencer/checkpoint_proposal_job.js +633 -62
  28. package/dest/sequencer/checkpoint_voter.d.ts +3 -2
  29. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  30. package/dest/sequencer/checkpoint_voter.js +34 -10
  31. package/dest/sequencer/index.d.ts +1 -3
  32. package/dest/sequencer/index.d.ts.map +1 -1
  33. package/dest/sequencer/index.js +0 -2
  34. package/dest/sequencer/metrics.d.ts +19 -7
  35. package/dest/sequencer/metrics.d.ts.map +1 -1
  36. package/dest/sequencer/metrics.js +131 -141
  37. package/dest/sequencer/sequencer.d.ts +38 -18
  38. package/dest/sequencer/sequencer.d.ts.map +1 -1
  39. package/dest/sequencer/sequencer.js +513 -66
  40. package/dest/sequencer/timetable.d.ts +1 -4
  41. package/dest/sequencer/timetable.d.ts.map +1 -1
  42. package/dest/sequencer/timetable.js +1 -4
  43. package/dest/test/index.d.ts +4 -7
  44. package/dest/test/index.d.ts.map +1 -1
  45. package/dest/test/mock_checkpoint_builder.d.ts +25 -11
  46. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  47. package/dest/test/mock_checkpoint_builder.js +52 -9
  48. package/dest/test/utils.d.ts +13 -9
  49. package/dest/test/utils.d.ts.map +1 -1
  50. package/dest/test/utils.js +27 -17
  51. package/package.json +30 -28
  52. package/src/client/sequencer-client.ts +28 -11
  53. package/src/config.ts +31 -19
  54. package/src/global_variable_builder/global_builder.ts +14 -14
  55. package/src/index.ts +1 -9
  56. package/src/publisher/config.ts +112 -43
  57. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  58. package/src/publisher/sequencer-publisher-metrics.ts +17 -69
  59. package/src/publisher/sequencer-publisher.ts +180 -118
  60. package/src/sequencer/checkpoint_proposal_job.ts +299 -91
  61. package/src/sequencer/checkpoint_voter.ts +32 -7
  62. package/src/sequencer/index.ts +0 -2
  63. package/src/sequencer/metrics.ts +132 -148
  64. package/src/sequencer/sequencer.ts +159 -68
  65. package/src/sequencer/timetable.ts +6 -5
  66. package/src/test/index.ts +3 -6
  67. package/src/test/mock_checkpoint_builder.ts +102 -29
  68. package/src/test/utils.ts +58 -28
  69. package/dest/sequencer/block_builder.d.ts +0 -26
  70. package/dest/sequencer/block_builder.d.ts.map +0 -1
  71. package/dest/sequencer/block_builder.js +0 -129
  72. package/dest/sequencer/checkpoint_builder.d.ts +0 -63
  73. package/dest/sequencer/checkpoint_builder.d.ts.map +0 -1
  74. package/dest/sequencer/checkpoint_builder.js +0 -131
  75. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  76. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  77. package/dest/tx_validator/nullifier_cache.js +0 -24
  78. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  79. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  80. package/dest/tx_validator/tx_validator_factory.js +0 -53
  81. package/src/sequencer/block_builder.ts +0 -217
  82. package/src/sequencer/checkpoint_builder.ts +0 -217
  83. package/src/tx_validator/nullifier_cache.ts +0 -30
  84. package/src/tx_validator/tx_validator_factory.ts +0 -133
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
+ FeeAssetPriceOracle,
7
8
  type GovernanceProposerContract,
8
9
  type IEmpireBase,
9
10
  MULTI_CALL_3_ADDRESS,
@@ -18,33 +19,35 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
23
+ MAX_L1_TX_LIMIT,
21
24
  type TransactionStats,
22
25
  WEI_CONST,
23
26
  } from '@aztec/ethereum/l1-tx-utils';
24
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
- import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
27
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
28
  import { sumBigint } from '@aztec/foundation/bigint';
27
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
- import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
30
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
29
31
  import { pick } from '@aztec/foundation/collection';
30
32
  import type { Fr } from '@aztec/foundation/curves/bn254';
31
33
  import { EthAddress } from '@aztec/foundation/eth-address';
32
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
33
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
34
37
  import { bufferToHex } from '@aztec/foundation/string';
35
38
  import { DateProvider, Timer } from '@aztec/foundation/timer';
36
39
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
37
40
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
38
- import { CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
41
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
39
42
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
40
43
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
41
44
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
42
45
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
43
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
46
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
44
47
 
45
48
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
46
49
 
47
- import type { PublisherConfig, TxSenderConfig } from './config.js';
50
+ import type { SequencerPublisherConfig } from './config.js';
48
51
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
49
52
 
50
53
  /** Arguments to the process method of the rollup contract */
@@ -59,6 +62,8 @@ type L1ProcessArgs = {
59
62
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
63
  /** Attestations and signers signature */
61
64
  attestationsAndSignersSignature: Signature;
65
+ /** The fee asset price modifier in basis points (from oracle) */
66
+ feeAssetPriceModifier: bigint;
62
67
  };
63
68
 
64
69
  export const Actions = [
@@ -80,12 +85,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
80
85
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
81
86
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
82
87
 
83
- export type InvalidateBlockRequest = {
88
+ export type InvalidateCheckpointRequest = {
84
89
  request: L1TxRequest;
85
90
  reason: 'invalid-attestation' | 'insufficient-attestations';
86
91
  gasUsed: bigint;
87
- blockNumber: BlockNumber;
88
- forcePendingBlockNumber: BlockNumber;
92
+ checkpointNumber: CheckpointNumber;
93
+ forcePendingCheckpointNumber: CheckpointNumber;
89
94
  };
90
95
 
91
96
  interface RequestWithExpiry {
@@ -111,6 +116,7 @@ export class SequencerPublisher {
111
116
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
112
117
 
113
118
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
119
+ private payloadProposedCache: Set<string> = new Set<string>();
114
120
 
115
121
  protected log: Logger;
116
122
  protected ethereumSlotDuration: bigint;
@@ -122,10 +128,9 @@ export class SequencerPublisher {
122
128
 
123
129
  /** L1 fee analyzer for fisherman mode */
124
130
  private l1FeeAnalyzer?: L1FeeAnalyzer;
125
- // @note - with blobs, the below estimate seems too large.
126
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
127
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
128
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
131
+
132
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
133
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
129
134
 
130
135
  // A CALL to a cold address is 2700 gas
131
136
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -133,20 +138,23 @@ export class SequencerPublisher {
133
138
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
134
139
  public static VOTE_GAS_GUESS: bigint = 800_000n;
135
140
 
136
- public l1TxUtils: L1TxUtilsWithBlobs;
141
+ public l1TxUtils: L1TxUtils;
137
142
  public rollupContract: RollupContract;
138
143
  public govProposerContract: GovernanceProposerContract;
139
144
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
140
145
  public slashFactoryContract: SlashFactoryContract;
141
146
 
147
+ public readonly tracer: Tracer;
148
+
142
149
  protected requests: RequestWithExpiry[] = [];
143
150
 
144
151
  constructor(
145
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
152
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
153
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
146
154
  deps: {
147
155
  telemetry?: TelemetryClient;
148
156
  blobClient: BlobClientInterface;
149
- l1TxUtils: L1TxUtilsWithBlobs;
157
+ l1TxUtils: L1TxUtils;
150
158
  rollupContract: RollupContract;
151
159
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
152
160
  governanceProposerContract: GovernanceProposerContract;
@@ -167,6 +175,7 @@ export class SequencerPublisher {
167
175
 
168
176
  const telemetry = deps.telemetry ?? getTelemetryClient();
169
177
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
178
+ this.tracer = telemetry.getTracer('SequencerPublisher');
170
179
  this.l1TxUtils = deps.l1TxUtils;
171
180
 
172
181
  this.rollupContract = deps.rollupContract;
@@ -189,12 +198,27 @@ export class SequencerPublisher {
189
198
  createLogger('sequencer:publisher:fee-analyzer'),
190
199
  );
191
200
  }
201
+
202
+ // Initialize fee asset price oracle
203
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
204
+ this.l1TxUtils.client,
205
+ this.rollupContract,
206
+ createLogger('sequencer:publisher:price-oracle'),
207
+ );
192
208
  }
193
209
 
194
210
  public getRollupContract(): RollupContract {
195
211
  return this.rollupContract;
196
212
  }
197
213
 
214
+ /**
215
+ * Gets the fee asset price modifier from the oracle.
216
+ * Returns 0n if the oracle query fails.
217
+ */
218
+ public getFeeAssetPriceModifier(): Promise<bigint> {
219
+ return this.feeAssetPriceOracle.computePriceModifier();
220
+ }
221
+
198
222
  public getSenderAddress() {
199
223
  return this.l1TxUtils.getSenderAddress();
200
224
  }
@@ -270,7 +294,7 @@ export class SequencerPublisher {
270
294
  // Start the analysis
271
295
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
272
296
  l2SlotNumber,
273
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
297
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
274
298
  l1Requests,
275
299
  blobConfig,
276
300
  onComplete,
@@ -296,6 +320,7 @@ export class SequencerPublisher {
296
320
  * - a receipt and errorMsg if it failed on L1
297
321
  * - undefined if no valid requests are found OR the tx failed to send.
298
322
  */
323
+ @trackSpan('SequencerPublisher.sendRequests')
299
324
  public async sendRequests() {
300
325
  const requestsToProcess = [...this.requests];
301
326
  this.requests = [];
@@ -342,7 +367,16 @@ export class SequencerPublisher {
342
367
 
343
368
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
344
369
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
345
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
370
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
371
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
372
+ const maxGas = MAX_L1_TX_LIMIT;
373
+ if (gasLimit !== undefined && gasLimit > maxGas) {
374
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
375
+ requested: gasLimit,
376
+ capped: maxGas,
377
+ });
378
+ gasLimit = maxGas;
379
+ }
346
380
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
347
381
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
348
382
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -413,17 +447,14 @@ export class SequencerPublisher {
413
447
  public canProposeAtNextEthBlock(
414
448
  tipArchive: Fr,
415
449
  msgSender: EthAddress,
416
- opts: { forcePendingBlockNumber?: BlockNumber } = {},
450
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
417
451
  ) {
418
452
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
419
453
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
420
454
 
421
455
  return this.rollupContract
422
456
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
423
- forcePendingCheckpointNumber:
424
- opts.forcePendingBlockNumber !== undefined
425
- ? CheckpointNumber.fromBlockNumber(opts.forcePendingBlockNumber)
426
- : undefined,
457
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
427
458
  })
428
459
  .catch(err => {
429
460
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -442,10 +473,11 @@ export class SequencerPublisher {
442
473
  * It will throw if the block header is invalid.
443
474
  * @param header - The block header to validate
444
475
  */
476
+ @trackSpan('SequencerPublisher.validateBlockHeader')
445
477
  public async validateBlockHeader(
446
478
  header: CheckpointHeader,
447
- opts?: { forcePendingBlockNumber: BlockNumber | undefined },
448
- ) {
479
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
480
+ ): Promise<void> {
449
481
  const flags = { ignoreDA: true, ignoreSignatures: true };
450
482
 
451
483
  const args = [
@@ -454,17 +486,13 @@ export class SequencerPublisher {
454
486
  [], // no signers
455
487
  Signature.empty().toViemSignature(),
456
488
  `0x${'0'.repeat(64)}`, // 32 empty bytes
457
- header.contentCommitment.blobsHash.toString(),
489
+ header.blobsHash.toString(),
458
490
  flags,
459
491
  ] as const;
460
492
 
461
493
  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
494
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
467
- optsForcePendingCheckpointNumber,
495
+ opts?.forcePendingCheckpointNumber,
468
496
  );
469
497
  let balance = 0n;
470
498
  if (this.config.fishermanMode) {
@@ -492,77 +520,95 @@ export class SequencerPublisher {
492
520
  }
493
521
 
494
522
  /**
495
- * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
496
- * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
523
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
524
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
497
525
  */
498
- public async simulateInvalidateBlock(
499
- validationResult: ValidateBlockResult,
500
- ): Promise<InvalidateBlockRequest | undefined> {
526
+ public async simulateInvalidateCheckpoint(
527
+ validationResult: ValidateCheckpointResult,
528
+ ): Promise<InvalidateCheckpointRequest | undefined> {
501
529
  if (validationResult.valid) {
502
530
  return undefined;
503
531
  }
504
532
 
505
- const { reason, block } = validationResult;
506
- const blockNumber = block.blockNumber;
507
- const logData = { ...block, reason };
533
+ const { reason, checkpoint } = validationResult;
534
+ const checkpointNumber = checkpoint.checkpointNumber;
535
+ const logData = { ...checkpoint, reason };
508
536
 
509
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
510
- if (currentBlockNumber < validationResult.block.blockNumber) {
537
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
538
+ if (currentCheckpointNumber < checkpointNumber) {
511
539
  this.log.verbose(
512
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
513
- { currentBlockNumber, ...logData },
540
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
541
+ { currentCheckpointNumber, ...logData },
514
542
  );
515
543
  return undefined;
516
544
  }
517
545
 
518
- const request = this.buildInvalidateBlockRequest(validationResult);
519
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
546
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
547
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
520
548
 
521
549
  try {
522
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
523
- this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
550
+ const { gasUsed } = await this.l1TxUtils.simulate(
551
+ request,
552
+ undefined,
553
+ undefined,
554
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
555
+ );
556
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
557
+ ...logData,
558
+ request,
559
+ gasUsed,
560
+ });
524
561
 
525
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: BlockNumber(blockNumber - 1), reason };
562
+ return {
563
+ request,
564
+ gasUsed,
565
+ checkpointNumber,
566
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
567
+ reason,
568
+ };
526
569
  } catch (err) {
527
570
  const viemError = formatViemError(err);
528
571
 
529
- // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
530
- // we can safely ignore it and return undefined so we go ahead with block building.
531
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
572
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
573
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
574
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
532
575
  this.log.verbose(
533
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
576
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
534
577
  { ...logData, request, error: viemError.message },
535
578
  );
536
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
537
- if (latestPendingBlockNumber < blockNumber) {
538
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
579
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
580
+ if (latestPendingCheckpointNumber < checkpointNumber) {
581
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
539
582
  return undefined;
540
583
  } else {
541
584
  this.log.error(
542
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
585
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
543
586
  viemError,
544
587
  logData,
545
588
  );
546
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
547
- cause: viemError,
548
- });
589
+ throw new Error(
590
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
591
+ {
592
+ cause: viemError,
593
+ },
594
+ );
549
595
  }
550
596
  }
551
597
 
552
- // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
553
- this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
554
- throw new Error(`Failed to simulate invalidate block ${blockNumber}`, { cause: viemError });
598
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
599
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
600
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
555
601
  }
556
602
  }
557
603
 
558
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
604
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
559
605
  if (validationResult.valid) {
560
- throw new Error('Cannot invalidate a valid block');
606
+ throw new Error('Cannot invalidate a valid checkpoint');
561
607
  }
562
608
 
563
- const { block, committee, reason } = validationResult;
564
- const logData = { ...block, reason };
565
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
609
+ const { checkpoint, committee, reason } = validationResult;
610
+ const logData = { ...checkpoint, reason };
611
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
566
612
 
567
613
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
568
614
  validationResult.attestations,
@@ -570,14 +616,14 @@ export class SequencerPublisher {
570
616
 
571
617
  if (reason === 'invalid-attestation') {
572
618
  return this.rollupContract.buildInvalidateBadAttestationRequest(
573
- CheckpointNumber.fromBlockNumber(block.blockNumber),
619
+ checkpoint.checkpointNumber,
574
620
  attestationsAndSigners,
575
621
  committee,
576
622
  validationResult.invalidIndex,
577
623
  );
578
624
  } else if (reason === 'insufficient-attestations') {
579
625
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
580
- CheckpointNumber.fromBlockNumber(block.blockNumber),
626
+ checkpoint.checkpointNumber,
581
627
  attestationsAndSigners,
582
628
  committee,
583
629
  );
@@ -588,31 +634,16 @@ export class SequencerPublisher {
588
634
  }
589
635
 
590
636
  /** Simulates `propose` to make sure that the checkpoint is valid for submission */
637
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
591
638
  public async validateCheckpointForSubmission(
592
639
  checkpoint: Checkpoint,
593
640
  attestationsAndSigners: CommitteeAttestationsAndSigners,
594
641
  attestationsAndSignersSignature: Signature,
595
- options: { forcePendingBlockNumber?: BlockNumber }, // TODO(palla/mbps): Should this be forcePendingCheckpointNumber?
642
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
596
643
  ): Promise<bigint> {
597
644
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
598
-
599
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
600
- // If we have no attestations, we still need to provide the empty attestations
601
- // so that the committee is recalculated correctly
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
645
  const blobFields = checkpoint.toBlobFields();
615
- const blobs = getBlobsPerL1Block(blobFields);
646
+ const blobs = await getBlobsPerL1Block(blobFields);
616
647
  const blobInput = getPrefixedEthBlobCommitments(blobs);
617
648
 
618
649
  const args = [
@@ -620,7 +651,7 @@ export class SequencerPublisher {
620
651
  header: checkpoint.header.toViem(),
621
652
  archive: toHex(checkpoint.archive.root.toBuffer()),
622
653
  oracleInput: {
623
- feeAssetPriceModifier: 0n,
654
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
624
655
  },
625
656
  },
626
657
  attestationsAndSigners.getPackedAttestations(),
@@ -669,6 +700,32 @@ export class SequencerPublisher {
669
700
  return false;
670
701
  }
671
702
 
703
+ // Check if payload was already submitted to governance
704
+ const cacheKey = payload.toString();
705
+ if (!this.payloadProposedCache.has(cacheKey)) {
706
+ try {
707
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
708
+ const proposed = await retry(
709
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
710
+ 'Check if payload was proposed',
711
+ makeBackoff([0, 1, 2]),
712
+ this.log,
713
+ true,
714
+ );
715
+ if (proposed) {
716
+ this.payloadProposedCache.add(cacheKey);
717
+ }
718
+ } catch (err) {
719
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
720
+ return false;
721
+ }
722
+ }
723
+
724
+ if (this.payloadProposedCache.has(cacheKey)) {
725
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
726
+ return false;
727
+ }
728
+
672
729
  const cachedLastVote = this.lastActions[signalType];
673
730
  this.lastActions[signalType] = slotNumber;
674
731
  const action = signalType;
@@ -688,7 +745,7 @@ export class SequencerPublisher {
688
745
  });
689
746
 
690
747
  try {
691
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
748
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
692
749
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
693
750
  } catch (err) {
694
751
  this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
@@ -891,19 +948,20 @@ export class SequencerPublisher {
891
948
  checkpoint: Checkpoint,
892
949
  attestationsAndSigners: CommitteeAttestationsAndSigners,
893
950
  attestationsAndSignersSignature: Signature,
894
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
951
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
895
952
  ): Promise<void> {
896
953
  const checkpointHeader = checkpoint.header;
897
954
 
898
955
  const blobFields = checkpoint.toBlobFields();
899
- const blobs = getBlobsPerL1Block(blobFields);
956
+ const blobs = await getBlobsPerL1Block(blobFields);
900
957
 
901
- const proposeTxArgs = {
958
+ const proposeTxArgs: L1ProcessArgs = {
902
959
  header: checkpointHeader,
903
960
  archive: checkpoint.archive.root.toBuffer(),
904
961
  blobs,
905
962
  attestationsAndSigners,
906
963
  attestationsAndSignersSignature,
964
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
907
965
  };
908
966
 
909
967
  let ts: bigint;
@@ -924,7 +982,7 @@ export class SequencerPublisher {
924
982
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
925
983
  ...checkpoint.getStats(),
926
984
  slotNumber: checkpoint.header.slotNumber,
927
- forcePendingBlockNumber: opts.forcePendingBlockNumber,
985
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
928
986
  });
929
987
  throw err;
930
988
  }
@@ -933,7 +991,10 @@ export class SequencerPublisher {
933
991
  await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
934
992
  }
935
993
 
936
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
994
+ public enqueueInvalidateCheckpoint(
995
+ request: InvalidateCheckpointRequest | undefined,
996
+ opts: { txTimeoutAt?: Date } = {},
997
+ ) {
937
998
  if (!request) {
938
999
  return;
939
1000
  }
@@ -941,9 +1002,9 @@ export class SequencerPublisher {
941
1002
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
942
1003
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
943
1004
 
944
- const { gasUsed, blockNumber } = request;
945
- const logData = { gasUsed, blockNumber, gasLimit, opts };
946
- this.log.verbose(`Enqueuing invalidate block request`, logData);
1005
+ const { gasUsed, checkpointNumber } = request;
1006
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
1007
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
947
1008
  this.addRequest({
948
1009
  action: `invalidate-by-${request.reason}`,
949
1010
  request: request.request,
@@ -956,9 +1017,9 @@ export class SequencerPublisher {
956
1017
  result.receipt.status === 'success' &&
957
1018
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
958
1019
  if (!success) {
959
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1020
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
960
1021
  } else {
961
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1022
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
962
1023
  }
963
1024
  return !!success;
964
1025
  },
@@ -984,12 +1045,14 @@ export class SequencerPublisher {
984
1045
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
985
1046
 
986
1047
  let gasUsed: bigint;
1048
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
987
1049
  try {
988
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1050
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
989
1051
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
990
1052
  } catch (err) {
991
- const viemError = formatViemError(err);
1053
+ const viemError = formatViemError(err, simulateAbi);
992
1054
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1055
+
993
1056
  return false;
994
1057
  }
995
1058
 
@@ -997,10 +1060,14 @@ export class SequencerPublisher {
997
1060
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
998
1061
  logData.gasLimit = gasLimit;
999
1062
 
1063
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1064
+ // when the tx is sent and a revert is diagnosed via simulation.
1065
+ const requestWithAbi = { ...request, abi: simulateAbi };
1066
+
1000
1067
  this.log.debug(`Enqueuing ${action}`, logData);
1001
1068
  this.addRequest({
1002
1069
  action,
1003
- request,
1070
+ request: requestWithAbi,
1004
1071
  gasConfig: { gasLimit },
1005
1072
  lastValidL2Slot: slotNumber,
1006
1073
  checkSuccess: (_req, result) => {
@@ -1037,7 +1104,7 @@ export class SequencerPublisher {
1037
1104
  private async prepareProposeTx(
1038
1105
  encodedData: L1ProcessArgs,
1039
1106
  timestamp: bigint,
1040
- options: { forcePendingBlockNumber?: BlockNumber },
1107
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1041
1108
  ) {
1042
1109
  const kzg = Blob.getViemKzgInstance();
1043
1110
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -1082,8 +1149,7 @@ export class SequencerPublisher {
1082
1149
  header: encodedData.header.toViem(),
1083
1150
  archive: toHex(encodedData.archive),
1084
1151
  oracleInput: {
1085
- // We are currently not modifying these. See #9963
1086
- feeAssetPriceModifier: 0n,
1152
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1087
1153
  },
1088
1154
  },
1089
1155
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1109,7 +1175,7 @@ export class SequencerPublisher {
1109
1175
  readonly header: ViemHeader;
1110
1176
  readonly archive: `0x${string}`;
1111
1177
  readonly oracleInput: {
1112
- readonly feeAssetPriceModifier: 0n;
1178
+ readonly feeAssetPriceModifier: bigint;
1113
1179
  };
1114
1180
  },
1115
1181
  ViemCommitteeAttestations,
@@ -1118,7 +1184,7 @@ export class SequencerPublisher {
1118
1184
  `0x${string}`,
1119
1185
  ],
1120
1186
  timestamp: bigint,
1121
- options: { forcePendingBlockNumber?: BlockNumber },
1187
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1122
1188
  ) {
1123
1189
  const rollupData = encodeFunctionData({
1124
1190
  abi: RollupAbi,
@@ -1127,13 +1193,9 @@ export class SequencerPublisher {
1127
1193
  });
1128
1194
 
1129
1195
  // override the pending checkpoint number if requested
1130
- const optsForcePendingCheckpointNumber =
1131
- options.forcePendingBlockNumber !== undefined
1132
- ? CheckpointNumber.fromBlockNumber(options.forcePendingBlockNumber)
1133
- : undefined;
1134
1196
  const forcePendingCheckpointNumberStateDiff = (
1135
- optsForcePendingCheckpointNumber !== undefined
1136
- ? await this.rollupContract.makePendingCheckpointNumberOverride(optsForcePendingCheckpointNumber)
1197
+ options.forcePendingCheckpointNumber !== undefined
1198
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1137
1199
  : []
1138
1200
  ).flatMap(override => override.stateDiff ?? []);
1139
1201
 
@@ -1160,20 +1222,20 @@ export class SequencerPublisher {
1160
1222
  {
1161
1223
  to: this.rollupContract.address,
1162
1224
  data: rollupData,
1163
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1225
+ gas: MAX_L1_TX_LIMIT,
1164
1226
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1165
1227
  },
1166
1228
  {
1167
1229
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1168
1230
  time: timestamp + 1n,
1169
1231
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1170
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1232
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1171
1233
  },
1172
1234
  stateOverrides,
1173
1235
  RollupAbi,
1174
1236
  {
1175
1237
  // @note fallback gas estimate to use if the node doesn't support simulation API
1176
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1238
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1177
1239
  },
1178
1240
  )
1179
1241
  .catch(err => {
@@ -1183,7 +1245,7 @@ export class SequencerPublisher {
1183
1245
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1184
1246
  // Return a minimal simulation result with the fallback gas estimate
1185
1247
  return {
1186
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1248
+ gasUsed: MAX_L1_TX_LIMIT,
1187
1249
  logs: [],
1188
1250
  };
1189
1251
  }
@@ -1197,7 +1259,7 @@ export class SequencerPublisher {
1197
1259
  private async addProposeTx(
1198
1260
  checkpoint: Checkpoint,
1199
1261
  encodedData: L1ProcessArgs,
1200
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: BlockNumber } = {},
1262
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1201
1263
  timestamp: bigint,
1202
1264
  ): Promise<void> {
1203
1265
  const slot = checkpoint.header.slotNumber;