@aztec/sequencer-client 0.0.1-commit.5476d83 → 0.0.1-commit.5914bae

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 (99) hide show
  1. package/dest/client/sequencer-client.d.ts +25 -16
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +69 -29
  4. package/dest/config.d.ts +34 -8
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +105 -44
  7. package/dest/global_variable_builder/global_builder.d.ts +27 -14
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +65 -54
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/index.d.ts +2 -3
  13. package/dest/index.d.ts.map +1 -1
  14. package/dest/index.js +1 -2
  15. package/dest/publisher/config.d.ts +37 -20
  16. package/dest/publisher/config.d.ts.map +1 -1
  17. package/dest/publisher/config.js +104 -39
  18. package/dest/publisher/sequencer-publisher-factory.d.ts +15 -6
  19. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher-factory.js +14 -3
  21. package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -3
  22. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher-metrics.js +23 -86
  24. package/dest/publisher/sequencer-publisher.d.ts +76 -53
  25. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher.js +691 -169
  27. package/dest/sequencer/checkpoint_proposal_job.d.ts +102 -0
  28. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  29. package/dest/sequencer/checkpoint_proposal_job.js +1247 -0
  30. package/dest/sequencer/checkpoint_voter.d.ts +34 -0
  31. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
  32. package/dest/sequencer/checkpoint_voter.js +106 -0
  33. package/dest/sequencer/config.d.ts +3 -2
  34. package/dest/sequencer/config.d.ts.map +1 -1
  35. package/dest/sequencer/events.d.ts +46 -0
  36. package/dest/sequencer/events.d.ts.map +1 -0
  37. package/dest/sequencer/events.js +1 -0
  38. package/dest/sequencer/index.d.ts +4 -2
  39. package/dest/sequencer/index.d.ts.map +1 -1
  40. package/dest/sequencer/index.js +3 -1
  41. package/dest/sequencer/metrics.d.ts +38 -6
  42. package/dest/sequencer/metrics.d.ts.map +1 -1
  43. package/dest/sequencer/metrics.js +216 -72
  44. package/dest/sequencer/sequencer.d.ts +125 -133
  45. package/dest/sequencer/sequencer.d.ts.map +1 -1
  46. package/dest/sequencer/sequencer.js +733 -640
  47. package/dest/sequencer/timetable.d.ts +54 -16
  48. package/dest/sequencer/timetable.d.ts.map +1 -1
  49. package/dest/sequencer/timetable.js +147 -62
  50. package/dest/sequencer/types.d.ts +3 -0
  51. package/dest/sequencer/types.d.ts.map +1 -0
  52. package/dest/sequencer/types.js +1 -0
  53. package/dest/sequencer/utils.d.ts +14 -8
  54. package/dest/sequencer/utils.d.ts.map +1 -1
  55. package/dest/sequencer/utils.js +7 -4
  56. package/dest/test/index.d.ts +6 -7
  57. package/dest/test/index.d.ts.map +1 -1
  58. package/dest/test/mock_checkpoint_builder.d.ts +95 -0
  59. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
  60. package/dest/test/mock_checkpoint_builder.js +233 -0
  61. package/dest/test/utils.d.ts +53 -0
  62. package/dest/test/utils.d.ts.map +1 -0
  63. package/dest/test/utils.js +104 -0
  64. package/package.json +32 -30
  65. package/src/client/sequencer-client.ts +99 -53
  66. package/src/config.ts +121 -54
  67. package/src/global_variable_builder/global_builder.ts +76 -73
  68. package/src/global_variable_builder/index.ts +1 -1
  69. package/src/index.ts +1 -7
  70. package/src/publisher/config.ts +122 -50
  71. package/src/publisher/sequencer-publisher-factory.ts +28 -10
  72. package/src/publisher/sequencer-publisher-metrics.ts +19 -71
  73. package/src/publisher/sequencer-publisher.ts +406 -215
  74. package/src/sequencer/README.md +531 -0
  75. package/src/sequencer/checkpoint_proposal_job.ts +976 -0
  76. package/src/sequencer/checkpoint_voter.ts +119 -0
  77. package/src/sequencer/config.ts +2 -1
  78. package/src/sequencer/events.ts +27 -0
  79. package/src/sequencer/index.ts +3 -1
  80. package/src/sequencer/metrics.ts +268 -82
  81. package/src/sequencer/sequencer.ts +481 -845
  82. package/src/sequencer/timetable.ts +178 -83
  83. package/src/sequencer/types.ts +6 -0
  84. package/src/sequencer/utils.ts +18 -9
  85. package/src/test/index.ts +5 -6
  86. package/src/test/mock_checkpoint_builder.ts +325 -0
  87. package/src/test/utils.ts +167 -0
  88. package/dest/sequencer/block_builder.d.ts +0 -27
  89. package/dest/sequencer/block_builder.d.ts.map +0 -1
  90. package/dest/sequencer/block_builder.js +0 -134
  91. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  92. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  93. package/dest/tx_validator/nullifier_cache.js +0 -24
  94. package/dest/tx_validator/tx_validator_factory.d.ts +0 -17
  95. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  96. package/dest/tx_validator/tx_validator_factory.js +0 -53
  97. package/src/sequencer/block_builder.ts +0 -222
  98. package/src/tx_validator/nullifier_cache.ts +0 -30
  99. package/src/tx_validator/tx_validator_factory.ts +0 -132
@@ -1,48 +1,55 @@
1
- import { L2Block } from '@aztec/aztec.js/block';
1
+ import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/blob-lib';
3
- import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
4
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
+ import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
- FormattedViemError,
7
+ FeeAssetPriceOracle,
8
8
  type GovernanceProposerContract,
9
9
  type IEmpireBase,
10
- type L1BlobInputs,
11
- type L1ContractsConfig,
12
- type L1TxConfig,
13
- type L1TxRequest,
14
10
  MULTI_CALL_3_ADDRESS,
15
11
  Multicall3,
16
12
  RollupContract,
17
13
  type TallySlashingProposerContract,
18
- type TransactionStats,
19
14
  type ViemCommitteeAttestations,
20
15
  type ViemHeader,
16
+ } from '@aztec/ethereum/contracts';
17
+ import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
18
+ import {
19
+ type L1BlobInputs,
20
+ type L1TxConfig,
21
+ type L1TxRequest,
22
+ type L1TxUtils,
23
+ MAX_L1_TX_LIMIT,
24
+ type TransactionStats,
21
25
  WEI_CONST,
22
- formatViemError,
23
- tryExtractEvent,
24
- } from '@aztec/ethereum';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
+ } from '@aztec/ethereum/l1-tx-utils';
27
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
28
  import { sumBigint } from '@aztec/foundation/bigint';
27
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
- import { SlotNumber } from '@aztec/foundation/branded-types';
30
+ import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
32
+ import { pick } from '@aztec/foundation/collection';
33
+ import type { Fr } from '@aztec/foundation/curves/bn254';
29
34
  import { EthAddress } from '@aztec/foundation/eth-address';
30
35
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
31
- import type { Fr } from '@aztec/foundation/fields';
32
36
  import { type Logger, createLogger } from '@aztec/foundation/log';
37
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
33
38
  import { bufferToHex } from '@aztec/foundation/string';
34
39
  import { DateProvider, Timer } from '@aztec/foundation/timer';
35
40
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
36
41
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
37
- import { CommitteeAttestation, CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
42
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
43
+ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
44
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
38
45
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
39
46
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
40
- import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
41
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
47
+ import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
48
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
42
49
 
43
50
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
44
51
 
45
- import type { PublisherConfig, TxSenderConfig } from './config.js';
52
+ import type { SequencerPublisherConfig } from './config.js';
46
53
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
47
54
 
48
55
  /** Arguments to the process method of the rollup contract */
@@ -57,6 +64,8 @@ type L1ProcessArgs = {
57
64
  attestationsAndSigners: CommitteeAttestationsAndSigners;
58
65
  /** Attestations and signers signature */
59
66
  attestationsAndSignersSignature: Signature;
67
+ /** The fee asset price modifier in basis points (from oracle) */
68
+ feeAssetPriceModifier: bigint;
60
69
  };
61
70
 
62
71
  export const Actions = [
@@ -78,12 +87,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
78
87
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
79
88
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
80
89
 
81
- export type InvalidateBlockRequest = {
90
+ export type InvalidateCheckpointRequest = {
82
91
  request: L1TxRequest;
83
92
  reason: 'invalid-attestation' | 'insufficient-attestations';
84
93
  gasUsed: bigint;
85
- blockNumber: number;
86
- forcePendingBlockNumber: number;
94
+ checkpointNumber: CheckpointNumber;
95
+ forcePendingCheckpointNumber: CheckpointNumber;
87
96
  };
88
97
 
89
98
  interface RequestWithExpiry {
@@ -108,17 +117,24 @@ export class SequencerPublisher {
108
117
 
109
118
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
110
119
 
120
+ private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
121
+ private payloadProposedCache: Set<string> = new Set<string>();
122
+
111
123
  protected log: Logger;
112
124
  protected ethereumSlotDuration: bigint;
125
+ protected aztecSlotDuration: bigint;
126
+ private dateProvider: DateProvider;
113
127
 
114
- private blobSinkClient: BlobSinkClientInterface;
128
+ private blobClient: BlobClientInterface;
115
129
 
116
130
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
117
131
  private proposerAddressForSimulation?: EthAddress;
118
- // @note - with blobs, the below estimate seems too large.
119
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
120
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
121
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
132
+
133
+ /** L1 fee analyzer for fisherman mode */
134
+ private l1FeeAnalyzer?: L1FeeAnalyzer;
135
+
136
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
137
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
122
138
 
123
139
  // A CALL to a cold address is 2700 gas
124
140
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -126,20 +142,23 @@ export class SequencerPublisher {
126
142
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
127
143
  public static VOTE_GAS_GUESS: bigint = 800_000n;
128
144
 
129
- public l1TxUtils: L1TxUtilsWithBlobs;
145
+ public l1TxUtils: L1TxUtils;
130
146
  public rollupContract: RollupContract;
131
147
  public govProposerContract: GovernanceProposerContract;
132
148
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
133
149
  public slashFactoryContract: SlashFactoryContract;
134
150
 
151
+ public readonly tracer: Tracer;
152
+
135
153
  protected requests: RequestWithExpiry[] = [];
136
154
 
137
155
  constructor(
138
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
156
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
157
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
139
158
  deps: {
140
159
  telemetry?: TelemetryClient;
141
- blobSinkClient?: BlobSinkClientInterface;
142
- l1TxUtils: L1TxUtilsWithBlobs;
160
+ blobClient: BlobClientInterface;
161
+ l1TxUtils: L1TxUtils;
143
162
  rollupContract: RollupContract;
144
163
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
145
164
  governanceProposerContract: GovernanceProposerContract;
@@ -153,14 +172,16 @@ export class SequencerPublisher {
153
172
  ) {
154
173
  this.log = deps.log ?? createLogger('sequencer:publisher');
155
174
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
175
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
176
+ this.dateProvider = deps.dateProvider;
156
177
  this.epochCache = deps.epochCache;
157
178
  this.lastActions = deps.lastActions;
158
179
 
159
- this.blobSinkClient =
160
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
180
+ this.blobClient = deps.blobClient;
161
181
 
162
182
  const telemetry = deps.telemetry ?? getTelemetryClient();
163
183
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
184
+ this.tracer = telemetry.getTracer('SequencerPublisher');
164
185
  this.l1TxUtils = deps.l1TxUtils;
165
186
 
166
187
  this.rollupContract = deps.rollupContract;
@@ -174,16 +195,47 @@ export class SequencerPublisher {
174
195
  this.slashingProposerContract = newSlashingProposer;
175
196
  });
176
197
  this.slashFactoryContract = deps.slashFactoryContract;
198
+
199
+ // Initialize L1 fee analyzer for fisherman mode
200
+ if (config.fishermanMode) {
201
+ this.l1FeeAnalyzer = new L1FeeAnalyzer(
202
+ this.l1TxUtils.client,
203
+ deps.dateProvider,
204
+ createLogger('sequencer:publisher:fee-analyzer'),
205
+ );
206
+ }
207
+
208
+ // Initialize fee asset price oracle
209
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
210
+ this.l1TxUtils.client,
211
+ this.rollupContract,
212
+ createLogger('sequencer:publisher:price-oracle'),
213
+ );
177
214
  }
178
215
 
179
216
  public getRollupContract(): RollupContract {
180
217
  return this.rollupContract;
181
218
  }
182
219
 
220
+ /**
221
+ * Gets the fee asset price modifier from the oracle.
222
+ * Returns 0n if the oracle query fails.
223
+ */
224
+ public getFeeAssetPriceModifier(): Promise<bigint> {
225
+ return this.feeAssetPriceOracle.computePriceModifier();
226
+ }
227
+
183
228
  public getSenderAddress() {
184
229
  return this.l1TxUtils.getSenderAddress();
185
230
  }
186
231
 
232
+ /**
233
+ * Gets the L1 fee analyzer instance (only available in fisherman mode)
234
+ */
235
+ public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
236
+ return this.l1FeeAnalyzer;
237
+ }
238
+
187
239
  /**
188
240
  * Sets the proposer address to use for simulations in fisherman mode.
189
241
  * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
@@ -211,6 +263,62 @@ export class SequencerPublisher {
211
263
  }
212
264
  }
213
265
 
266
+ /**
267
+ * Analyzes L1 fees for the pending requests without sending them.
268
+ * This is used in fisherman mode to validate fee calculations.
269
+ * @param l2SlotNumber - The L2 slot number for this analysis
270
+ * @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
271
+ * @returns The analysis result (incomplete until block mines), or undefined if no requests
272
+ */
273
+ public async analyzeL1Fees(
274
+ l2SlotNumber: SlotNumber,
275
+ onComplete?: (analysis: L1FeeAnalysisResult) => void,
276
+ ): Promise<L1FeeAnalysisResult | undefined> {
277
+ if (!this.l1FeeAnalyzer) {
278
+ this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
279
+ return undefined;
280
+ }
281
+
282
+ const requestsToAnalyze = [...this.requests];
283
+ if (requestsToAnalyze.length === 0) {
284
+ this.log.debug('No requests to analyze for L1 fees');
285
+ return undefined;
286
+ }
287
+
288
+ // Extract blob config from requests (if any)
289
+ const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
290
+ const blobConfig = blobConfigs[0];
291
+
292
+ // Get gas configs
293
+ const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
294
+ const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
295
+ const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
296
+
297
+ // Get the transaction requests
298
+ const l1Requests = requestsToAnalyze.map(r => r.request);
299
+
300
+ // Start the analysis
301
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(
302
+ l2SlotNumber,
303
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
304
+ l1Requests,
305
+ blobConfig,
306
+ onComplete,
307
+ );
308
+
309
+ this.log.info('Started L1 fee analysis', {
310
+ analysisId,
311
+ l2SlotNumber: l2SlotNumber.toString(),
312
+ requestCount: requestsToAnalyze.length,
313
+ hasBlobConfig: !!blobConfig,
314
+ gasLimit: gasLimit.toString(),
315
+ actions: requestsToAnalyze.map(r => r.action),
316
+ });
317
+
318
+ // Return the analysis result (will be incomplete until block mines)
319
+ return this.l1FeeAnalyzer.getAnalysis(analysisId);
320
+ }
321
+
214
322
  /**
215
323
  * Sends all requests that are still valid.
216
324
  * @returns one of:
@@ -218,10 +326,11 @@ export class SequencerPublisher {
218
326
  * - a receipt and errorMsg if it failed on L1
219
327
  * - undefined if no valid requests are found OR the tx failed to send.
220
328
  */
329
+ @trackSpan('SequencerPublisher.sendRequests')
221
330
  public async sendRequests() {
222
331
  const requestsToProcess = [...this.requests];
223
332
  this.requests = [];
224
- if (this.interrupted) {
333
+ if (this.interrupted || requestsToProcess.length === 0) {
225
334
  return undefined;
226
335
  }
227
336
  const currentL2Slot = this.getCurrentL2Slot();
@@ -253,8 +362,8 @@ export class SequencerPublisher {
253
362
  // @note - we can only have one blob config per bundle
254
363
  // find requests with gas and blob configs
255
364
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
256
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
257
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
365
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
366
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
258
367
 
259
368
  if (blobConfigs.length > 1) {
260
369
  throw new Error('Multiple blob configs found');
@@ -264,7 +373,16 @@ export class SequencerPublisher {
264
373
 
265
374
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
266
375
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
267
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
376
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
377
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
378
+ const maxGas = MAX_L1_TX_LIMIT;
379
+ if (gasLimit !== undefined && gasLimit > maxGas) {
380
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
381
+ requested: gasLimit,
382
+ capped: maxGas,
383
+ });
384
+ gasLimit = maxGas;
385
+ }
268
386
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
269
387
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
270
388
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -313,7 +431,16 @@ export class SequencerPublisher {
313
431
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
314
432
  return { failedActions: requests.map(r => r.action) };
315
433
  } else {
316
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
434
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
435
+ result,
436
+ requests: requests.map(r => ({
437
+ ...r,
438
+ // Avoid logging large blob data
439
+ blobConfig: r.blobConfig
440
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
441
+ : undefined,
442
+ })),
443
+ });
317
444
  const successfulActions: Action[] = [];
318
445
  const failedActions: Action[] = [];
319
446
  for (const request of requests) {
@@ -328,21 +455,25 @@ export class SequencerPublisher {
328
455
  }
329
456
 
330
457
  /**
331
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
458
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
332
459
  * @param tipArchive - The archive to check
333
460
  * @returns The slot and block number if it is possible to propose, undefined otherwise
334
461
  */
335
- public canProposeAtNextEthBlock(
462
+ public canProposeAt(
336
463
  tipArchive: Fr,
337
464
  msgSender: EthAddress,
338
- opts: { forcePendingBlockNumber?: number } = {},
465
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
339
466
  ) {
340
467
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
341
468
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
342
469
 
470
+ const pipelined = opts.pipelined ?? false;
471
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
472
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
473
+
343
474
  return this.rollupContract
344
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
345
- forcePendingCheckpointNumber: opts.forcePendingBlockNumber,
475
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
476
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
346
477
  })
347
478
  .catch(err => {
348
479
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -361,7 +492,11 @@ export class SequencerPublisher {
361
492
  * It will throw if the block header is invalid.
362
493
  * @param header - The block header to validate
363
494
  */
364
- public async validateBlockHeader(header: CheckpointHeader, opts?: { forcePendingBlockNumber: number | undefined }) {
495
+ @trackSpan('SequencerPublisher.validateBlockHeader')
496
+ public async validateBlockHeader(
497
+ header: CheckpointHeader,
498
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
499
+ ): Promise<void> {
365
500
  const flags = { ignoreDA: true, ignoreSignatures: true };
366
501
 
367
502
  const args = [
@@ -370,12 +505,14 @@ export class SequencerPublisher {
370
505
  [], // no signers
371
506
  Signature.empty().toViemSignature(),
372
507
  `0x${'0'.repeat(64)}`, // 32 empty bytes
373
- header.contentCommitment.blobsHash.toString(),
508
+ header.blobsHash.toString(),
374
509
  flags,
375
510
  ] as const;
376
511
 
377
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
378
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingBlockNumber);
512
+ const ts = this.getSimulationTimestamp(header.slotNumber);
513
+ const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
514
+ opts?.forcePendingCheckpointNumber,
515
+ );
379
516
  let balance = 0n;
380
517
  if (this.config.fishermanMode) {
381
518
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -395,84 +532,102 @@ export class SequencerPublisher {
395
532
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
396
533
  from: MULTI_CALL_3_ADDRESS,
397
534
  },
398
- { time: ts + 1n },
535
+ { time: ts },
399
536
  stateOverrides,
400
537
  );
401
538
  this.log.debug(`Simulated validateHeader`);
402
539
  }
403
540
 
404
541
  /**
405
- * Simulate making a call to invalidate a block with invalid attestations. Returns undefined if no need to invalidate.
406
- * @param block - The block to invalidate and the criteria for invalidation (as returned by the archiver)
542
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
543
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
407
544
  */
408
- public async simulateInvalidateBlock(
409
- validationResult: ValidateBlockResult,
410
- ): Promise<InvalidateBlockRequest | undefined> {
545
+ public async simulateInvalidateCheckpoint(
546
+ validationResult: ValidateCheckpointResult,
547
+ ): Promise<InvalidateCheckpointRequest | undefined> {
411
548
  if (validationResult.valid) {
412
549
  return undefined;
413
550
  }
414
551
 
415
- const { reason, block } = validationResult;
416
- const blockNumber = block.blockNumber;
417
- const logData = { ...block, reason };
552
+ const { reason, checkpoint } = validationResult;
553
+ const checkpointNumber = checkpoint.checkpointNumber;
554
+ const logData = { ...checkpoint, reason };
418
555
 
419
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
420
- if (currentBlockNumber < validationResult.block.blockNumber) {
556
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
557
+ if (currentCheckpointNumber < checkpointNumber) {
421
558
  this.log.verbose(
422
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
423
- { currentBlockNumber, ...logData },
559
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
560
+ { currentCheckpointNumber, ...logData },
424
561
  );
425
562
  return undefined;
426
563
  }
427
564
 
428
- const request = this.buildInvalidateBlockRequest(validationResult);
429
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
565
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
566
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
430
567
 
431
568
  try {
432
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
433
- this.log.verbose(`Simulation for invalidate block ${blockNumber} succeeded`, { ...logData, request, gasUsed });
569
+ const { gasUsed } = await this.l1TxUtils.simulate(
570
+ request,
571
+ undefined,
572
+ undefined,
573
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
574
+ );
575
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
576
+ ...logData,
577
+ request,
578
+ gasUsed,
579
+ });
434
580
 
435
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
581
+ return {
582
+ request,
583
+ gasUsed,
584
+ checkpointNumber,
585
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
586
+ reason,
587
+ };
436
588
  } catch (err) {
437
589
  const viemError = formatViemError(err);
438
590
 
439
- // If the error is due to the block not being in the pending chain, and it was indeed removed by someone else,
440
- // we can safely ignore it and return undefined so we go ahead with block building.
441
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
591
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
592
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
593
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
442
594
  this.log.verbose(
443
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
595
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
444
596
  { ...logData, request, error: viemError.message },
445
597
  );
446
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
447
- if (latestPendingBlockNumber < blockNumber) {
448
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
598
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
599
+ if (latestPendingCheckpointNumber < checkpointNumber) {
600
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
449
601
  return undefined;
450
602
  } else {
451
603
  this.log.error(
452
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
604
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
453
605
  viemError,
454
606
  logData,
455
607
  );
456
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
457
- cause: viemError,
458
- });
608
+ throw new Error(
609
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
610
+ {
611
+ cause: viemError,
612
+ },
613
+ );
459
614
  }
460
615
  }
461
616
 
462
- // Otherwise, throw. We cannot build the next block if we cannot invalidate the previous one.
463
- this.log.error(`Simulation for invalidate block ${blockNumber} failed`, viemError, logData);
464
- throw new Error(`Failed to simulate invalidate block ${blockNumber}`, { cause: viemError });
617
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
618
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
619
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
465
620
  }
466
621
  }
467
622
 
468
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
623
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
469
624
  if (validationResult.valid) {
470
- throw new Error('Cannot invalidate a valid block');
625
+ throw new Error('Cannot invalidate a valid checkpoint');
471
626
  }
472
627
 
473
- const { block, committee, reason } = validationResult;
474
- const logData = { ...block, reason };
475
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
628
+ const { checkpoint, committee, reason } = validationResult;
629
+ const logData = { ...checkpoint, reason };
630
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
476
631
 
477
632
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
478
633
  validationResult.attestations,
@@ -480,14 +635,14 @@ export class SequencerPublisher {
480
635
 
481
636
  if (reason === 'invalid-attestation') {
482
637
  return this.rollupContract.buildInvalidateBadAttestationRequest(
483
- block.blockNumber,
638
+ checkpoint.checkpointNumber,
484
639
  attestationsAndSigners,
485
640
  committee,
486
641
  validationResult.invalidIndex,
487
642
  );
488
643
  } else if (reason === 'insufficient-attestations') {
489
644
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
490
- block.blockNumber,
645
+ checkpoint.checkpointNumber,
491
646
  attestationsAndSigners,
492
647
  committee,
493
648
  );
@@ -497,47 +652,24 @@ export class SequencerPublisher {
497
652
  }
498
653
  }
499
654
 
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,
655
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */
656
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
657
+ public async validateCheckpointForSubmission(
658
+ checkpoint: Checkpoint,
511
659
  attestationsAndSigners: CommitteeAttestationsAndSigners,
512
660
  attestationsAndSignersSignature: Signature,
513
- options: { forcePendingBlockNumber?: number },
514
- ): Promise<bigint> {
515
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
516
-
517
- // If we have no attestations, we still need to provide the empty attestations
518
- // 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();
532
- const blobs = getBlobsPerL1Block(blobFields);
661
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
662
+ ): Promise<void> {
663
+ const blobFields = checkpoint.toBlobFields();
664
+ const blobs = await getBlobsPerL1Block(blobFields);
533
665
  const blobInput = getPrefixedEthBlobCommitments(blobs);
534
666
 
535
667
  const args = [
536
668
  {
537
- header: block.getCheckpointHeader().toViem(),
538
- archive: toHex(block.archive.root.toBuffer()),
669
+ header: checkpoint.header.toViem(),
670
+ archive: toHex(checkpoint.archive.root.toBuffer()),
539
671
  oracleInput: {
540
- feeAssetPriceModifier: 0n,
672
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
541
673
  },
542
674
  },
543
675
  attestationsAndSigners.getPackedAttestations(),
@@ -546,13 +678,11 @@ export class SequencerPublisher {
546
678
  blobInput,
547
679
  ] as const;
548
680
 
549
- await this.simulateProposeTx(args, ts, options);
550
- return ts;
681
+ await this.simulateProposeTx(args, options);
551
682
  }
552
683
 
553
684
  private async enqueueCastSignalHelper(
554
685
  slotNumber: SlotNumber,
555
- timestamp: bigint,
556
686
  signalType: GovernanceSignalAction,
557
687
  payload: EthAddress,
558
688
  base: IEmpireBase,
@@ -573,10 +703,45 @@ export class SequencerPublisher {
573
703
  const round = await base.computeRound(slotNumber);
574
704
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
575
705
 
706
+ if (roundInfo.quorumReached) {
707
+ return false;
708
+ }
709
+
576
710
  if (roundInfo.lastSignalSlot >= slotNumber) {
577
711
  return false;
578
712
  }
579
713
 
714
+ if (await this.isPayloadEmpty(payload)) {
715
+ this.log.warn(`Skipping vote cast for payload with empty code`);
716
+ return false;
717
+ }
718
+
719
+ // Check if payload was already submitted to governance
720
+ const cacheKey = payload.toString();
721
+ if (!this.payloadProposedCache.has(cacheKey)) {
722
+ try {
723
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
724
+ const proposed = await retry(
725
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
726
+ 'Check if payload was proposed',
727
+ makeBackoff([0, 1, 2]),
728
+ this.log,
729
+ true,
730
+ );
731
+ if (proposed) {
732
+ this.payloadProposedCache.add(cacheKey);
733
+ }
734
+ } catch (err) {
735
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
736
+ return false;
737
+ }
738
+ }
739
+
740
+ if (this.payloadProposedCache.has(cacheKey)) {
741
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
742
+ return false;
743
+ }
744
+
580
745
  const cachedLastVote = this.lastActions[signalType];
581
746
  this.lastActions[signalType] = slotNumber;
582
747
  const action = signalType;
@@ -595,11 +760,16 @@ export class SequencerPublisher {
595
760
  lastValidL2Slot: slotNumber,
596
761
  });
597
762
 
763
+ const timestamp = this.getSimulationTimestamp(slotNumber);
764
+
598
765
  try {
599
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
766
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
600
767
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
601
768
  } catch (err) {
602
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
769
+ const viemError = formatViemError(err);
770
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
771
+ simulationTimestamp: timestamp,
772
+ });
603
773
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
604
774
  }
605
775
 
@@ -619,14 +789,14 @@ export class SequencerPublisher {
619
789
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
620
790
  if (!success) {
621
791
  this.log.error(
622
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`,
792
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
623
793
  logData,
624
794
  );
625
795
  this.lastActions[signalType] = cachedLastVote;
626
796
  return false;
627
797
  } else {
628
798
  this.log.info(
629
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
799
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
630
800
  logData,
631
801
  );
632
802
  return true;
@@ -636,22 +806,30 @@ export class SequencerPublisher {
636
806
  return true;
637
807
  }
638
808
 
809
+ private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
810
+ const key = payload.toString();
811
+ const cached = this.isPayloadEmptyCache.get(key);
812
+ if (cached) {
813
+ return cached;
814
+ }
815
+ const isEmpty = !(await this.l1TxUtils.getCode(payload));
816
+ this.isPayloadEmptyCache.set(key, isEmpty);
817
+ return isEmpty;
818
+ }
819
+
639
820
  /**
640
821
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
641
822
  * @param slotNumber - The slot number to cast a signal for.
642
- * @param timestamp - The timestamp of the slot to cast a signal for.
643
823
  * @returns True if the signal was successfully enqueued, false otherwise.
644
824
  */
645
825
  public enqueueGovernanceCastSignal(
646
826
  governancePayload: EthAddress,
647
827
  slotNumber: SlotNumber,
648
- timestamp: bigint,
649
828
  signerAddress: EthAddress,
650
829
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
651
830
  ): Promise<boolean> {
652
831
  return this.enqueueCastSignalHelper(
653
832
  slotNumber,
654
- timestamp,
655
833
  'governance-signal',
656
834
  governancePayload,
657
835
  this.govProposerContract,
@@ -664,7 +842,6 @@ export class SequencerPublisher {
664
842
  public async enqueueSlashingActions(
665
843
  actions: ProposerSlashAction[],
666
844
  slotNumber: SlotNumber,
667
- timestamp: bigint,
668
845
  signerAddress: EthAddress,
669
846
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
670
847
  ): Promise<boolean> {
@@ -685,7 +862,6 @@ export class SequencerPublisher {
685
862
  });
686
863
  await this.enqueueCastSignalHelper(
687
864
  slotNumber,
688
- timestamp,
689
865
  'empire-slashing-signal',
690
866
  action.payload,
691
867
  this.slashingProposerContract,
@@ -704,7 +880,6 @@ export class SequencerPublisher {
704
880
  (receipt: TransactionReceipt) =>
705
881
  !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
706
882
  slotNumber,
707
- timestamp,
708
883
  );
709
884
  break;
710
885
  }
@@ -722,7 +897,6 @@ export class SequencerPublisher {
722
897
  request,
723
898
  (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
724
899
  slotNumber,
725
- timestamp,
726
900
  );
727
901
  break;
728
902
  }
@@ -746,7 +920,6 @@ export class SequencerPublisher {
746
920
  request,
747
921
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
748
922
  slotNumber,
749
- timestamp,
750
923
  );
751
924
  break;
752
925
  }
@@ -768,7 +941,6 @@ export class SequencerPublisher {
768
941
  request,
769
942
  (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
770
943
  slotNumber,
771
- timestamp,
772
944
  );
773
945
  break;
774
946
  }
@@ -783,56 +955,56 @@ export class SequencerPublisher {
783
955
  return true;
784
956
  }
785
957
 
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,
958
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
959
+ public async enqueueProposeCheckpoint(
960
+ checkpoint: Checkpoint,
794
961
  attestationsAndSigners: CommitteeAttestationsAndSigners,
795
962
  attestationsAndSignersSignature: Signature,
796
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
797
- ): Promise<boolean> {
798
- const checkpointHeader = block.getCheckpointHeader();
963
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
964
+ ): Promise<void> {
965
+ const checkpointHeader = checkpoint.header;
799
966
 
800
- const blobFields = block.getCheckpointBlobFields();
801
- const blobs = getBlobsPerL1Block(blobFields);
967
+ const blobFields = checkpoint.toBlobFields();
968
+ const blobs = await getBlobsPerL1Block(blobFields);
802
969
 
803
- const proposeTxArgs = {
970
+ const proposeTxArgs: L1ProcessArgs = {
804
971
  header: checkpointHeader,
805
- archive: block.archive.root.toBuffer(),
806
- body: block.body.toBuffer(),
972
+ archive: checkpoint.archive.root.toBuffer(),
807
973
  blobs,
808
974
  attestationsAndSigners,
809
975
  attestationsAndSignersSignature,
976
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
810
977
  };
811
978
 
812
- let ts: bigint;
813
-
814
979
  try {
815
980
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
816
981
  // This means that we can avoid the simulation issues in later checks.
817
982
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
818
983
  // make time consistency checks break.
819
984
  // 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);
985
+ await this.validateCheckpointForSubmission(
986
+ checkpoint,
987
+ attestationsAndSigners,
988
+ attestationsAndSignersSignature,
989
+ opts,
990
+ );
821
991
  } 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,
825
- forcePendingBlockNumber: opts.forcePendingBlockNumber,
992
+ this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
993
+ ...checkpoint.getStats(),
994
+ slotNumber: checkpoint.header.slotNumber,
995
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
826
996
  });
827
997
  throw err;
828
998
  }
829
999
 
830
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
831
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
832
- return true;
1000
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1001
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts);
833
1002
  }
834
1003
 
835
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
1004
+ public enqueueInvalidateCheckpoint(
1005
+ request: InvalidateCheckpointRequest | undefined,
1006
+ opts: { txTimeoutAt?: Date } = {},
1007
+ ) {
836
1008
  if (!request) {
837
1009
  return;
838
1010
  }
@@ -840,9 +1012,9 @@ export class SequencerPublisher {
840
1012
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
841
1013
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
842
1014
 
843
- const { gasUsed, blockNumber } = request;
844
- const logData = { gasUsed, blockNumber, gasLimit, opts };
845
- this.log.verbose(`Enqueuing invalidate block request`, logData);
1015
+ const { gasUsed, checkpointNumber } = request;
1016
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
1017
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
846
1018
  this.addRequest({
847
1019
  action: `invalidate-by-${request.reason}`,
848
1020
  request: request.request,
@@ -855,9 +1027,9 @@ export class SequencerPublisher {
855
1027
  result.receipt.status === 'success' &&
856
1028
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
857
1029
  if (!success) {
858
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1030
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
859
1031
  } else {
860
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1032
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
861
1033
  }
862
1034
  return !!success;
863
1035
  },
@@ -869,8 +1041,8 @@ export class SequencerPublisher {
869
1041
  request: L1TxRequest,
870
1042
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
871
1043
  slotNumber: SlotNumber,
872
- timestamp: bigint,
873
1044
  ) {
1045
+ const timestamp = this.getSimulationTimestamp(slotNumber);
874
1046
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
875
1047
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
876
1048
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -883,12 +1055,15 @@ export class SequencerPublisher {
883
1055
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
884
1056
 
885
1057
  let gasUsed: bigint;
1058
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1059
+
886
1060
  try {
887
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1061
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
888
1062
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
889
1063
  } catch (err) {
890
- const viemError = formatViemError(err);
1064
+ const viemError = formatViemError(err, simulateAbi);
891
1065
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1066
+
892
1067
  return false;
893
1068
  }
894
1069
 
@@ -896,10 +1071,14 @@ export class SequencerPublisher {
896
1071
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
897
1072
  logData.gasLimit = gasLimit;
898
1073
 
1074
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1075
+ // when the tx is sent and a revert is diagnosed via simulation.
1076
+ const requestWithAbi = { ...request, abi: simulateAbi };
1077
+
899
1078
  this.log.debug(`Enqueuing ${action}`, logData);
900
1079
  this.addRequest({
901
1080
  action,
902
- request,
1081
+ request: requestWithAbi,
903
1082
  gasConfig: { gasLimit },
904
1083
  lastValidL2Slot: slotNumber,
905
1084
  checkSuccess: (_req, result) => {
@@ -935,8 +1114,7 @@ export class SequencerPublisher {
935
1114
 
936
1115
  private async prepareProposeTx(
937
1116
  encodedData: L1ProcessArgs,
938
- timestamp: bigint,
939
- options: { forcePendingBlockNumber?: number },
1117
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
940
1118
  ) {
941
1119
  const kzg = Blob.getViemKzgInstance();
942
1120
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -981,8 +1159,7 @@ export class SequencerPublisher {
981
1159
  header: encodedData.header.toViem(),
982
1160
  archive: toHex(encodedData.archive),
983
1161
  oracleInput: {
984
- // We are currently not modifying these. See #9963
985
- feeAssetPriceModifier: 0n,
1162
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
986
1163
  },
987
1164
  },
988
1165
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -991,7 +1168,7 @@ export class SequencerPublisher {
991
1168
  blobInput,
992
1169
  ] as const;
993
1170
 
994
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1171
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
995
1172
 
996
1173
  return { args, blobEvaluationGas, rollupData, simulationResult };
997
1174
  }
@@ -999,7 +1176,6 @@ export class SequencerPublisher {
999
1176
  /**
1000
1177
  * Simulates the propose tx with eth_simulateV1
1001
1178
  * @param args - The propose tx args
1002
- * @param timestamp - The timestamp to simulate proposal at
1003
1179
  * @returns The simulation result
1004
1180
  */
1005
1181
  private async simulateProposeTx(
@@ -1008,7 +1184,7 @@ export class SequencerPublisher {
1008
1184
  readonly header: ViemHeader;
1009
1185
  readonly archive: `0x${string}`;
1010
1186
  readonly oracleInput: {
1011
- readonly feeAssetPriceModifier: 0n;
1187
+ readonly feeAssetPriceModifier: bigint;
1012
1188
  };
1013
1189
  },
1014
1190
  ViemCommitteeAttestations,
@@ -1016,8 +1192,7 @@ export class SequencerPublisher {
1016
1192
  ViemSignature,
1017
1193
  `0x${string}`,
1018
1194
  ],
1019
- timestamp: bigint,
1020
- options: { forcePendingBlockNumber?: number },
1195
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1021
1196
  ) {
1022
1197
  const rollupData = encodeFunctionData({
1023
1198
  abi: RollupAbi,
@@ -1025,10 +1200,10 @@ export class SequencerPublisher {
1025
1200
  args,
1026
1201
  });
1027
1202
 
1028
- // override the pending block number if requested
1029
- const forcePendingBlockNumberStateDiff = (
1030
- options.forcePendingBlockNumber !== undefined
1031
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber)
1203
+ // override the pending checkpoint number if requested
1204
+ const forcePendingCheckpointNumberStateDiff = (
1205
+ options.forcePendingCheckpointNumber !== undefined
1206
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1032
1207
  : []
1033
1208
  ).flatMap(override => override.stateDiff ?? []);
1034
1209
 
@@ -1038,7 +1213,7 @@ export class SequencerPublisher {
1038
1213
  // @note we override checkBlob to false since blobs are not part simulate()
1039
1214
  stateDiff: [
1040
1215
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1041
- ...forcePendingBlockNumberStateDiff,
1216
+ ...forcePendingCheckpointNumberStateDiff,
1042
1217
  ],
1043
1218
  },
1044
1219
  ];
@@ -1050,25 +1225,26 @@ export class SequencerPublisher {
1050
1225
  });
1051
1226
  }
1052
1227
 
1228
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1229
+
1053
1230
  const simulationResult = await this.l1TxUtils
1054
1231
  .simulate(
1055
1232
  {
1056
1233
  to: this.rollupContract.address,
1057
1234
  data: rollupData,
1058
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1235
+ gas: MAX_L1_TX_LIMIT,
1059
1236
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1060
1237
  },
1061
1238
  {
1062
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1063
- time: timestamp + 1n,
1239
+ time: simTs,
1064
1240
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1065
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1241
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1066
1242
  },
1067
1243
  stateOverrides,
1068
1244
  RollupAbi,
1069
1245
  {
1070
1246
  // @note fallback gas estimate to use if the node doesn't support simulation API
1071
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1247
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1072
1248
  },
1073
1249
  )
1074
1250
  .catch(err => {
@@ -1078,11 +1254,11 @@ export class SequencerPublisher {
1078
1254
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1079
1255
  // Return a minimal simulation result with the fallback gas estimate
1080
1256
  return {
1081
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1257
+ gasUsed: MAX_L1_TX_LIMIT,
1082
1258
  logs: [],
1083
1259
  };
1084
1260
  }
1085
- this.log.error(`Failed to simulate propose tx`, viemError);
1261
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1086
1262
  throw err;
1087
1263
  });
1088
1264
 
@@ -1090,18 +1266,14 @@ export class SequencerPublisher {
1090
1266
  }
1091
1267
 
1092
1268
  private async addProposeTx(
1093
- block: L2Block,
1269
+ checkpoint: Checkpoint,
1094
1270
  encodedData: L1ProcessArgs,
1095
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
1096
- timestamp: bigint,
1271
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1097
1272
  ): Promise<void> {
1273
+ const slot = checkpoint.header.slotNumber;
1098
1274
  const timer = new Timer();
1099
1275
  const kzg = Blob.getViemKzgInstance();
1100
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1101
- encodedData,
1102
- timestamp,
1103
- opts,
1104
- );
1276
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1105
1277
  const startBlock = await this.l1TxUtils.getBlockNumber();
1106
1278
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1107
1279
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1109,11 +1281,13 @@ export class SequencerPublisher {
1109
1281
  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
1282
  );
1111
1283
 
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
- });
1284
+ // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1285
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
1286
+ void Promise.resolve().then(() =>
1287
+ this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
1288
+ this.log.error('Failed to send blobs to blob client');
1289
+ }),
1290
+ );
1117
1291
 
1118
1292
  return this.addRequest({
1119
1293
  action: 'propose',
@@ -1121,7 +1295,7 @@ export class SequencerPublisher {
1121
1295
  to: this.rollupContract.address,
1122
1296
  data: rollupData,
1123
1297
  },
1124
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1298
+ lastValidL2Slot: checkpoint.header.slotNumber,
1125
1299
  gasConfig: { ...opts, gasLimit },
1126
1300
  blobConfig: {
1127
1301
  blobs: encodedData.blobs.map(b => b.data),
@@ -1136,11 +1310,12 @@ export class SequencerPublisher {
1136
1310
  receipt &&
1137
1311
  receipt.status === 'success' &&
1138
1312
  tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1313
+
1139
1314
  if (success) {
1140
1315
  const endBlock = receipt.blockNumber;
1141
1316
  const inclusionBlocks = Number(endBlock - startBlock);
1142
1317
  const { calldataGas, calldataSize, sender } = stats!;
1143
- const publishStats: L1PublishBlockStats = {
1318
+ const publishStats: L1PublishCheckpointStats = {
1144
1319
  gasPrice: receipt.effectiveGasPrice,
1145
1320
  gasUsed: receipt.gasUsed,
1146
1321
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1149,26 +1324,42 @@ export class SequencerPublisher {
1149
1324
  calldataGas,
1150
1325
  calldataSize,
1151
1326
  sender,
1152
- ...block.getStats(),
1327
+ ...checkpoint.getStats(),
1153
1328
  eventName: 'rollup-published-to-l1',
1154
1329
  blobCount: encodedData.blobs.length,
1155
1330
  inclusionBlocks,
1156
1331
  };
1157
- this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats(), ...receipt });
1332
+ this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
1333
+ ...stats,
1334
+ ...checkpoint.getStats(),
1335
+ ...pick(receipt, 'transactionHash', 'blockHash'),
1336
+ });
1158
1337
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1159
1338
 
1160
1339
  return true;
1161
1340
  } else {
1162
1341
  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
- });
1342
+ this.log.error(
1343
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1344
+ undefined,
1345
+ { ...checkpoint.getStats(), ...receipt },
1346
+ );
1169
1347
  return false;
1170
1348
  }
1171
1349
  },
1172
1350
  });
1173
1351
  }
1352
+
1353
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1354
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1355
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1356
+ const l1Constants = this.epochCache.getL1Constants();
1357
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1358
+ }
1359
+
1360
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1361
+ private getNextL1SlotTimestamp(): bigint {
1362
+ const l1Constants = this.epochCache.getL1Constants();
1363
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1364
+ }
1174
1365
  }