@aztec/sequencer-client 0.0.1-commit.9593d84 → 0.0.1-commit.967fc6998

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 (115) hide show
  1. package/dest/client/sequencer-client.d.ts +21 -16
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +45 -27
  4. package/dest/config.d.ts +14 -8
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +83 -35
  7. package/dest/global_variable_builder/global_builder.d.ts +20 -13
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +51 -41
  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 +41 -20
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +109 -39
  16. package/dest/publisher/index.d.ts +2 -1
  17. package/dest/publisher/index.d.ts.map +1 -1
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  20. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  28. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  30. package/dest/publisher/sequencer-publisher-factory.d.ts +15 -6
  31. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  32. package/dest/publisher/sequencer-publisher-factory.js +14 -3
  33. package/dest/publisher/sequencer-publisher-metrics.d.ts +3 -3
  34. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  35. package/dest/publisher/sequencer-publisher-metrics.js +23 -86
  36. package/dest/publisher/sequencer-publisher.d.ts +69 -47
  37. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher.js +847 -145
  39. package/dest/sequencer/checkpoint_proposal_job.d.ts +102 -0
  40. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  41. package/dest/sequencer/checkpoint_proposal_job.js +1219 -0
  42. package/dest/sequencer/checkpoint_voter.d.ts +35 -0
  43. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
  44. package/dest/sequencer/checkpoint_voter.js +109 -0
  45. package/dest/sequencer/config.d.ts +3 -2
  46. package/dest/sequencer/config.d.ts.map +1 -1
  47. package/dest/sequencer/events.d.ts +46 -0
  48. package/dest/sequencer/events.d.ts.map +1 -0
  49. package/dest/sequencer/events.js +1 -0
  50. package/dest/sequencer/index.d.ts +4 -2
  51. package/dest/sequencer/index.d.ts.map +1 -1
  52. package/dest/sequencer/index.js +3 -1
  53. package/dest/sequencer/metrics.d.ts +38 -6
  54. package/dest/sequencer/metrics.d.ts.map +1 -1
  55. package/dest/sequencer/metrics.js +216 -72
  56. package/dest/sequencer/sequencer.d.ts +119 -133
  57. package/dest/sequencer/sequencer.d.ts.map +1 -1
  58. package/dest/sequencer/sequencer.js +717 -625
  59. package/dest/sequencer/timetable.d.ts +51 -14
  60. package/dest/sequencer/timetable.d.ts.map +1 -1
  61. package/dest/sequencer/timetable.js +145 -59
  62. package/dest/sequencer/types.d.ts +3 -0
  63. package/dest/sequencer/types.d.ts.map +1 -0
  64. package/dest/sequencer/types.js +1 -0
  65. package/dest/sequencer/utils.d.ts +14 -8
  66. package/dest/sequencer/utils.d.ts.map +1 -1
  67. package/dest/sequencer/utils.js +7 -4
  68. package/dest/test/index.d.ts +6 -7
  69. package/dest/test/index.d.ts.map +1 -1
  70. package/dest/test/mock_checkpoint_builder.d.ts +97 -0
  71. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
  72. package/dest/test/mock_checkpoint_builder.js +222 -0
  73. package/dest/test/utils.d.ts +53 -0
  74. package/dest/test/utils.d.ts.map +1 -0
  75. package/dest/test/utils.js +104 -0
  76. package/package.json +32 -30
  77. package/src/client/sequencer-client.ts +54 -47
  78. package/src/config.ts +95 -44
  79. package/src/global_variable_builder/global_builder.ts +65 -61
  80. package/src/index.ts +1 -7
  81. package/src/publisher/config.ts +131 -50
  82. package/src/publisher/index.ts +3 -0
  83. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  84. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  85. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  86. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  87. package/src/publisher/sequencer-publisher-factory.ts +28 -10
  88. package/src/publisher/sequencer-publisher-metrics.ts +19 -71
  89. package/src/publisher/sequencer-publisher.ts +528 -184
  90. package/src/sequencer/README.md +531 -0
  91. package/src/sequencer/checkpoint_proposal_job.ts +926 -0
  92. package/src/sequencer/checkpoint_voter.ts +130 -0
  93. package/src/sequencer/config.ts +2 -1
  94. package/src/sequencer/events.ts +27 -0
  95. package/src/sequencer/index.ts +3 -1
  96. package/src/sequencer/metrics.ts +268 -82
  97. package/src/sequencer/sequencer.ts +464 -831
  98. package/src/sequencer/timetable.ts +175 -80
  99. package/src/sequencer/types.ts +6 -0
  100. package/src/sequencer/utils.ts +18 -9
  101. package/src/test/index.ts +5 -6
  102. package/src/test/mock_checkpoint_builder.ts +320 -0
  103. package/src/test/utils.ts +167 -0
  104. package/dest/sequencer/block_builder.d.ts +0 -27
  105. package/dest/sequencer/block_builder.d.ts.map +0 -1
  106. package/dest/sequencer/block_builder.js +0 -134
  107. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  108. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  109. package/dest/tx_validator/nullifier_cache.js +0 -24
  110. package/dest/tx_validator/tx_validator_factory.d.ts +0 -17
  111. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  112. package/dest/tx_validator/tx_validator_factory.js +0 -53
  113. package/src/sequencer/block_builder.ts +0 -222
  114. package/src/tx_validator/nullifier_cache.ts +0 -30
  115. package/src/tx_validator/tx_validator_factory.ts +0 -132
@@ -1,48 +1,63 @@
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 { pick } from '@aztec/foundation/collection';
32
+ import type { Fr } from '@aztec/foundation/curves/bn254';
29
33
  import { EthAddress } from '@aztec/foundation/eth-address';
30
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
31
- import type { Fr } from '@aztec/foundation/fields';
32
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
33
37
  import { bufferToHex } from '@aztec/foundation/string';
34
38
  import { DateProvider, Timer } from '@aztec/foundation/timer';
35
39
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
36
40
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
37
- import { CommitteeAttestation, CommitteeAttestationsAndSigners, type ValidateBlockResult } from '@aztec/stdlib/block';
41
+ import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
42
+ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
38
43
  import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
39
44
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
40
- import type { L1PublishBlockStats } from '@aztec/stdlib/stats';
41
- import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
45
+ import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
46
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
42
47
 
43
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
44
-
45
- import type { PublisherConfig, TxSenderConfig } from './config.js';
48
+ import {
49
+ type Hex,
50
+ type StateOverride,
51
+ type TransactionReceipt,
52
+ type TypedDataDefinition,
53
+ encodeFunctionData,
54
+ keccak256,
55
+ multicall3Abi,
56
+ toHex,
57
+ } from 'viem';
58
+
59
+ import type { SequencerPublisherConfig } from './config.js';
60
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
46
61
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
47
62
 
48
63
  /** Arguments to the process method of the rollup contract */
@@ -57,6 +72,8 @@ type L1ProcessArgs = {
57
72
  attestationsAndSigners: CommitteeAttestationsAndSigners;
58
73
  /** Attestations and signers signature */
59
74
  attestationsAndSignersSignature: Signature;
75
+ /** The fee asset price modifier in basis points (from oracle) */
76
+ feeAssetPriceModifier: bigint;
60
77
  };
61
78
 
62
79
  export const Actions = [
@@ -78,12 +95,12 @@ type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slas
78
95
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
79
96
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
80
97
 
81
- export type InvalidateBlockRequest = {
98
+ export type InvalidateCheckpointRequest = {
82
99
  request: L1TxRequest;
83
100
  reason: 'invalid-attestation' | 'insufficient-attestations';
84
101
  gasUsed: bigint;
85
- blockNumber: number;
86
- forcePendingBlockNumber: number;
102
+ checkpointNumber: CheckpointNumber;
103
+ forcePendingCheckpointNumber: CheckpointNumber;
87
104
  };
88
105
 
89
106
  interface RequestWithExpiry {
@@ -102,23 +119,29 @@ export class SequencerPublisher {
102
119
  private interrupted = false;
103
120
  private metrics: SequencerPublisherMetrics;
104
121
  public epochCache: EpochCache;
122
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
105
123
 
106
124
  protected governanceLog = createLogger('sequencer:publisher:governance');
107
125
  protected slashingLog = createLogger('sequencer:publisher:slashing');
108
126
 
109
127
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
110
128
 
129
+ private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
130
+ private payloadProposedCache: Set<string> = new Set<string>();
131
+
111
132
  protected log: Logger;
112
133
  protected ethereumSlotDuration: bigint;
113
134
 
114
- private blobSinkClient: BlobSinkClientInterface;
135
+ private blobClient: BlobClientInterface;
115
136
 
116
137
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
117
138
  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;
139
+
140
+ /** L1 fee analyzer for fisherman mode */
141
+ private l1FeeAnalyzer?: L1FeeAnalyzer;
142
+
143
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
144
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
122
145
 
123
146
  // A CALL to a cold address is 2700 gas
124
147
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -126,20 +149,23 @@ export class SequencerPublisher {
126
149
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
127
150
  public static VOTE_GAS_GUESS: bigint = 800_000n;
128
151
 
129
- public l1TxUtils: L1TxUtilsWithBlobs;
152
+ public l1TxUtils: L1TxUtils;
130
153
  public rollupContract: RollupContract;
131
154
  public govProposerContract: GovernanceProposerContract;
132
155
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
133
156
  public slashFactoryContract: SlashFactoryContract;
134
157
 
158
+ public readonly tracer: Tracer;
159
+
135
160
  protected requests: RequestWithExpiry[] = [];
136
161
 
137
162
  constructor(
138
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
163
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
164
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
139
165
  deps: {
140
166
  telemetry?: TelemetryClient;
141
- blobSinkClient?: BlobSinkClientInterface;
142
- l1TxUtils: L1TxUtilsWithBlobs;
167
+ blobClient: BlobClientInterface;
168
+ l1TxUtils: L1TxUtils;
143
169
  rollupContract: RollupContract;
144
170
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
145
171
  governanceProposerContract: GovernanceProposerContract;
@@ -156,11 +182,11 @@ export class SequencerPublisher {
156
182
  this.epochCache = deps.epochCache;
157
183
  this.lastActions = deps.lastActions;
158
184
 
159
- this.blobSinkClient =
160
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('sequencer:blob-sink:client') });
185
+ this.blobClient = deps.blobClient;
161
186
 
162
187
  const telemetry = deps.telemetry ?? getTelemetryClient();
163
188
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
189
+ this.tracer = telemetry.getTracer('SequencerPublisher');
164
190
  this.l1TxUtils = deps.l1TxUtils;
165
191
 
166
192
  this.rollupContract = deps.rollupContract;
@@ -174,16 +200,72 @@ export class SequencerPublisher {
174
200
  this.slashingProposerContract = newSlashingProposer;
175
201
  });
176
202
  this.slashFactoryContract = deps.slashFactoryContract;
203
+
204
+ // Initialize L1 fee analyzer for fisherman mode
205
+ if (config.fishermanMode) {
206
+ this.l1FeeAnalyzer = new L1FeeAnalyzer(
207
+ this.l1TxUtils.client,
208
+ deps.dateProvider,
209
+ createLogger('sequencer:publisher:fee-analyzer'),
210
+ );
211
+ }
212
+
213
+ // Initialize fee asset price oracle
214
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
215
+ this.l1TxUtils.client,
216
+ this.rollupContract,
217
+ createLogger('sequencer:publisher:price-oracle'),
218
+ );
219
+
220
+ // Initialize failed L1 tx store (optional, for test networks)
221
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
222
+ }
223
+
224
+ /**
225
+ * Backs up a failed L1 transaction to the configured store for debugging.
226
+ * Does nothing if no store is configured.
227
+ */
228
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
229
+ if (!this.failedTxStore) {
230
+ return;
231
+ }
232
+
233
+ const tx: FailedL1Tx = {
234
+ ...failedTx,
235
+ timestamp: Date.now(),
236
+ };
237
+
238
+ // Fire and forget - don't block on backup
239
+ void this.failedTxStore
240
+ .then(store => store?.saveFailedTx(tx))
241
+ .catch(err => {
242
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
243
+ });
177
244
  }
178
245
 
179
246
  public getRollupContract(): RollupContract {
180
247
  return this.rollupContract;
181
248
  }
182
249
 
250
+ /**
251
+ * Gets the fee asset price modifier from the oracle.
252
+ * Returns 0n if the oracle query fails.
253
+ */
254
+ public getFeeAssetPriceModifier(): Promise<bigint> {
255
+ return this.feeAssetPriceOracle.computePriceModifier();
256
+ }
257
+
183
258
  public getSenderAddress() {
184
259
  return this.l1TxUtils.getSenderAddress();
185
260
  }
186
261
 
262
+ /**
263
+ * Gets the L1 fee analyzer instance (only available in fisherman mode)
264
+ */
265
+ public getL1FeeAnalyzer(): L1FeeAnalyzer | undefined {
266
+ return this.l1FeeAnalyzer;
267
+ }
268
+
187
269
  /**
188
270
  * Sets the proposer address to use for simulations in fisherman mode.
189
271
  * @param proposerAddress - The actual proposer's address to use for balance lookups in simulations
@@ -211,6 +293,62 @@ export class SequencerPublisher {
211
293
  }
212
294
  }
213
295
 
296
+ /**
297
+ * Analyzes L1 fees for the pending requests without sending them.
298
+ * This is used in fisherman mode to validate fee calculations.
299
+ * @param l2SlotNumber - The L2 slot number for this analysis
300
+ * @param onComplete - Optional callback to invoke when analysis completes (after block is mined)
301
+ * @returns The analysis result (incomplete until block mines), or undefined if no requests
302
+ */
303
+ public async analyzeL1Fees(
304
+ l2SlotNumber: SlotNumber,
305
+ onComplete?: (analysis: L1FeeAnalysisResult) => void,
306
+ ): Promise<L1FeeAnalysisResult | undefined> {
307
+ if (!this.l1FeeAnalyzer) {
308
+ this.log.warn('L1 fee analyzer not available (not in fisherman mode)');
309
+ return undefined;
310
+ }
311
+
312
+ const requestsToAnalyze = [...this.requests];
313
+ if (requestsToAnalyze.length === 0) {
314
+ this.log.debug('No requests to analyze for L1 fees');
315
+ return undefined;
316
+ }
317
+
318
+ // Extract blob config from requests (if any)
319
+ const blobConfigs = requestsToAnalyze.filter(request => request.blobConfig).map(request => request.blobConfig);
320
+ const blobConfig = blobConfigs[0];
321
+
322
+ // Get gas configs
323
+ const gasConfigs = requestsToAnalyze.filter(request => request.gasConfig).map(request => request.gasConfig);
324
+ const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
325
+ const gasLimit = gasLimits.length > 0 ? gasLimits.reduce((sum, g) => sum + g, 0n) : 0n;
326
+
327
+ // Get the transaction requests
328
+ const l1Requests = requestsToAnalyze.map(r => r.request);
329
+
330
+ // Start the analysis
331
+ const analysisId = await this.l1FeeAnalyzer.startAnalysis(
332
+ l2SlotNumber,
333
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
334
+ l1Requests,
335
+ blobConfig,
336
+ onComplete,
337
+ );
338
+
339
+ this.log.info('Started L1 fee analysis', {
340
+ analysisId,
341
+ l2SlotNumber: l2SlotNumber.toString(),
342
+ requestCount: requestsToAnalyze.length,
343
+ hasBlobConfig: !!blobConfig,
344
+ gasLimit: gasLimit.toString(),
345
+ actions: requestsToAnalyze.map(r => r.action),
346
+ });
347
+
348
+ // Return the analysis result (will be incomplete until block mines)
349
+ return this.l1FeeAnalyzer.getAnalysis(analysisId);
350
+ }
351
+
214
352
  /**
215
353
  * Sends all requests that are still valid.
216
354
  * @returns one of:
@@ -218,10 +356,11 @@ export class SequencerPublisher {
218
356
  * - a receipt and errorMsg if it failed on L1
219
357
  * - undefined if no valid requests are found OR the tx failed to send.
220
358
  */
359
+ @trackSpan('SequencerPublisher.sendRequests')
221
360
  public async sendRequests() {
222
361
  const requestsToProcess = [...this.requests];
223
362
  this.requests = [];
224
- if (this.interrupted) {
363
+ if (this.interrupted || requestsToProcess.length === 0) {
225
364
  return undefined;
226
365
  }
227
366
  const currentL2Slot = this.getCurrentL2Slot();
@@ -264,7 +403,16 @@ export class SequencerPublisher {
264
403
 
265
404
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
266
405
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
267
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
406
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
407
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
408
+ const maxGas = MAX_L1_TX_LIMIT;
409
+ if (gasLimit !== undefined && gasLimit > maxGas) {
410
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
411
+ requested: gasLimit,
412
+ capped: maxGas,
413
+ });
414
+ gasLimit = maxGas;
415
+ }
268
416
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
269
417
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
270
418
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -274,6 +422,21 @@ export class SequencerPublisher {
274
422
  validRequests.sort((a, b) => compareActions(a.action, b.action));
275
423
 
276
424
  try {
425
+ // Capture context for failed tx backup before sending
426
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
427
+ const multicallData = encodeFunctionData({
428
+ abi: multicall3Abi,
429
+ functionName: 'aggregate3',
430
+ args: [
431
+ validRequests.map(r => ({
432
+ target: r.request.to!,
433
+ callData: r.request.data!,
434
+ allowFailure: true,
435
+ })),
436
+ ],
437
+ });
438
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
439
+
277
440
  this.log.debug('Forwarding transactions', {
278
441
  validRequests: validRequests.map(request => request.action),
279
442
  txConfig,
@@ -286,7 +449,12 @@ export class SequencerPublisher {
286
449
  this.rollupContract.address,
287
450
  this.log,
288
451
  );
289
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
452
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
453
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
454
+ validRequests,
455
+ result,
456
+ txContext,
457
+ );
290
458
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
291
459
  } catch (err) {
292
460
  const viemError = formatViemError(err);
@@ -306,11 +474,25 @@ export class SequencerPublisher {
306
474
 
307
475
  private callbackBundledTransactions(
308
476
  requests: RequestWithExpiry[],
309
- result?: { receipt: TransactionReceipt } | FormattedViemError,
477
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
478
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
310
479
  ) {
311
480
  const actionsListStr = requests.map(r => r.action).join(', ');
312
481
  if (result instanceof FormattedViemError) {
313
482
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
483
+ this.backupFailedTx({
484
+ id: keccak256(txContext.multicallData),
485
+ failureType: 'send-error',
486
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
487
+ blobData: txContext.blobData,
488
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
489
+ error: { message: result.message, name: result.name },
490
+ context: {
491
+ actions: requests.map(r => r.action),
492
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
493
+ sender: this.getSenderAddress().toString(),
494
+ },
495
+ });
314
496
  return { failedActions: requests.map(r => r.action) };
315
497
  } else {
316
498
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -323,6 +505,30 @@ export class SequencerPublisher {
323
505
  failedActions.push(request.action);
324
506
  }
325
507
  }
508
+ // Single backup for the whole reverted tx
509
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
510
+ this.backupFailedTx({
511
+ id: result.receipt.transactionHash,
512
+ failureType: 'revert',
513
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
514
+ blobData: txContext.blobData,
515
+ l1BlockNumber: result.receipt.blockNumber.toString(),
516
+ receipt: {
517
+ transactionHash: result.receipt.transactionHash,
518
+ blockNumber: result.receipt.blockNumber.toString(),
519
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
520
+ status: 'reverted',
521
+ },
522
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
523
+ context: {
524
+ actions: failedActions,
525
+ requests: requests
526
+ .filter(r => failedActions.includes(r.action))
527
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
528
+ sender: this.getSenderAddress().toString(),
529
+ },
530
+ });
531
+ }
326
532
  return { successfulActions, failedActions };
327
533
  }
328
534
  }
@@ -335,14 +541,14 @@ export class SequencerPublisher {
335
541
  public canProposeAtNextEthBlock(
336
542
  tipArchive: Fr,
337
543
  msgSender: EthAddress,
338
- opts: { forcePendingBlockNumber?: number } = {},
544
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
339
545
  ) {
340
546
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
341
547
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
342
548
 
343
549
  return this.rollupContract
344
550
  .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
345
- forcePendingCheckpointNumber: opts.forcePendingBlockNumber,
551
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
346
552
  })
347
553
  .catch(err => {
348
554
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -361,7 +567,11 @@ export class SequencerPublisher {
361
567
  * It will throw if the block header is invalid.
362
568
  * @param header - The block header to validate
363
569
  */
364
- public async validateBlockHeader(header: CheckpointHeader, opts?: { forcePendingBlockNumber: number | undefined }) {
570
+ @trackSpan('SequencerPublisher.validateBlockHeader')
571
+ public async validateBlockHeader(
572
+ header: CheckpointHeader,
573
+ opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
574
+ ): Promise<void> {
365
575
  const flags = { ignoreDA: true, ignoreSignatures: true };
366
576
 
367
577
  const args = [
@@ -370,12 +580,14 @@ export class SequencerPublisher {
370
580
  [], // no signers
371
581
  Signature.empty().toViemSignature(),
372
582
  `0x${'0'.repeat(64)}`, // 32 empty bytes
373
- header.contentCommitment.blobsHash.toString(),
583
+ header.blobsHash.toString(),
374
584
  flags,
375
585
  ] as const;
376
586
 
377
587
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
378
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(opts?.forcePendingBlockNumber);
588
+ const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
589
+ opts?.forcePendingCheckpointNumber,
590
+ );
379
591
  let balance = 0n;
380
592
  if (this.config.fishermanMode) {
381
593
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -402,77 +614,109 @@ export class SequencerPublisher {
402
614
  }
403
615
 
404
616
  /**
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)
617
+ * Simulate making a call to invalidate a checkpoint with invalid attestations. Returns undefined if no need to invalidate.
618
+ * @param validationResult - The validation result indicating which checkpoint to invalidate (as returned by the archiver)
407
619
  */
408
- public async simulateInvalidateBlock(
409
- validationResult: ValidateBlockResult,
410
- ): Promise<InvalidateBlockRequest | undefined> {
620
+ public async simulateInvalidateCheckpoint(
621
+ validationResult: ValidateCheckpointResult,
622
+ ): Promise<InvalidateCheckpointRequest | undefined> {
411
623
  if (validationResult.valid) {
412
624
  return undefined;
413
625
  }
414
626
 
415
- const { reason, block } = validationResult;
416
- const blockNumber = block.blockNumber;
417
- const logData = { ...block, reason };
627
+ const { reason, checkpoint } = validationResult;
628
+ const checkpointNumber = checkpoint.checkpointNumber;
629
+ const logData = { ...checkpoint, reason };
418
630
 
419
- const currentBlockNumber = await this.rollupContract.getCheckpointNumber();
420
- if (currentBlockNumber < validationResult.block.blockNumber) {
631
+ const currentCheckpointNumber = await this.rollupContract.getCheckpointNumber();
632
+ if (currentCheckpointNumber < checkpointNumber) {
421
633
  this.log.verbose(
422
- `Skipping block ${blockNumber} invalidation since it has already been removed from the pending chain`,
423
- { currentBlockNumber, ...logData },
634
+ `Skipping checkpoint ${checkpointNumber} invalidation since it has already been removed from the pending chain`,
635
+ { currentCheckpointNumber, ...logData },
424
636
  );
425
637
  return undefined;
426
638
  }
427
639
 
428
- const request = this.buildInvalidateBlockRequest(validationResult);
429
- this.log.debug(`Simulating invalidate block ${blockNumber}`, { ...logData, request });
640
+ const request = this.buildInvalidateCheckpointRequest(validationResult);
641
+ this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
642
+
643
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
430
644
 
431
645
  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 });
646
+ const { gasUsed } = await this.l1TxUtils.simulate(
647
+ request,
648
+ undefined,
649
+ undefined,
650
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
651
+ );
652
+ this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
653
+ ...logData,
654
+ request,
655
+ gasUsed,
656
+ });
434
657
 
435
- return { request, gasUsed, blockNumber, forcePendingBlockNumber: blockNumber - 1, reason };
658
+ return {
659
+ request,
660
+ gasUsed,
661
+ checkpointNumber,
662
+ forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
663
+ reason,
664
+ };
436
665
  } catch (err) {
437
666
  const viemError = formatViemError(err);
438
667
 
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')) {
668
+ // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
669
+ // we can safely ignore it and return undefined so we go ahead with checkpoint building.
670
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
442
671
  this.log.verbose(
443
- `Simulation for invalidate block ${blockNumber} failed due to block not being in pending chain`,
672
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
444
673
  { ...logData, request, error: viemError.message },
445
674
  );
446
- const latestPendingBlockNumber = await this.rollupContract.getCheckpointNumber();
447
- if (latestPendingBlockNumber < blockNumber) {
448
- this.log.verbose(`Block number ${blockNumber} has already been invalidated`, { ...logData });
675
+ const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
676
+ if (latestPendingCheckpointNumber < checkpointNumber) {
677
+ this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
449
678
  return undefined;
450
679
  } else {
451
680
  this.log.error(
452
- `Simulation for invalidate ${blockNumber} failed and it is still in pending chain`,
681
+ `Simulation for invalidate checkpoint ${checkpointNumber} failed and it is still in pending chain`,
453
682
  viemError,
454
683
  logData,
455
684
  );
456
- throw new Error(`Failed to simulate invalidate block ${blockNumber} while it is still in pending chain`, {
457
- cause: viemError,
458
- });
685
+ throw new Error(
686
+ `Failed to simulate invalidate checkpoint ${checkpointNumber} while it is still in pending chain`,
687
+ {
688
+ cause: viemError,
689
+ },
690
+ );
459
691
  }
460
692
  }
461
693
 
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 });
694
+ // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
695
+ this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
696
+ this.backupFailedTx({
697
+ id: keccak256(request.data!),
698
+ failureType: 'simulation',
699
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
700
+ l1BlockNumber: l1BlockNumber.toString(),
701
+ error: { message: viemError.message, name: viemError.name },
702
+ context: {
703
+ actions: [`invalidate-${reason}`],
704
+ checkpointNumber,
705
+ sender: this.getSenderAddress().toString(),
706
+ },
707
+ });
708
+ throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
465
709
  }
466
710
  }
467
711
 
468
- private buildInvalidateBlockRequest(validationResult: ValidateBlockResult) {
712
+ private buildInvalidateCheckpointRequest(validationResult: ValidateCheckpointResult) {
469
713
  if (validationResult.valid) {
470
- throw new Error('Cannot invalidate a valid block');
714
+ throw new Error('Cannot invalidate a valid checkpoint');
471
715
  }
472
716
 
473
- const { block, committee, reason } = validationResult;
474
- const logData = { ...block, reason };
475
- this.log.debug(`Simulating invalidate block ${block.blockNumber}`, logData);
717
+ const { checkpoint, committee, reason } = validationResult;
718
+ const logData = { ...checkpoint, reason };
719
+ this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
476
720
 
477
721
  const attestationsAndSigners = new CommitteeAttestationsAndSigners(
478
722
  validationResult.attestations,
@@ -480,14 +724,14 @@ export class SequencerPublisher {
480
724
 
481
725
  if (reason === 'invalid-attestation') {
482
726
  return this.rollupContract.buildInvalidateBadAttestationRequest(
483
- block.blockNumber,
727
+ checkpoint.checkpointNumber,
484
728
  attestationsAndSigners,
485
729
  committee,
486
730
  validationResult.invalidIndex,
487
731
  );
488
732
  } else if (reason === 'insufficient-attestations') {
489
733
  return this.rollupContract.buildInvalidateInsufficientAttestationsRequest(
490
- block.blockNumber,
734
+ checkpoint.checkpointNumber,
491
735
  attestationsAndSigners,
492
736
  committee,
493
737
  );
@@ -497,47 +741,25 @@ export class SequencerPublisher {
497
741
  }
498
742
  }
499
743
 
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,
744
+ /** Simulates `propose` to make sure that the checkpoint is valid for submission */
745
+ @trackSpan('SequencerPublisher.validateCheckpointForSubmission')
746
+ public async validateCheckpointForSubmission(
747
+ checkpoint: Checkpoint,
511
748
  attestationsAndSigners: CommitteeAttestationsAndSigners,
512
749
  attestationsAndSignersSignature: Signature,
513
- options: { forcePendingBlockNumber?: number },
750
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
514
751
  ): Promise<bigint> {
515
752
  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);
753
+ const blobFields = checkpoint.toBlobFields();
754
+ const blobs = await getBlobsPerL1Block(blobFields);
533
755
  const blobInput = getPrefixedEthBlobCommitments(blobs);
534
756
 
535
757
  const args = [
536
758
  {
537
- header: block.getCheckpointHeader().toViem(),
538
- archive: toHex(block.archive.root.toBuffer()),
759
+ header: checkpoint.header.toViem(),
760
+ archive: toHex(checkpoint.archive.root.toBuffer()),
539
761
  oracleInput: {
540
- feeAssetPriceModifier: 0n,
762
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
541
763
  },
542
764
  },
543
765
  attestationsAndSigners.getPackedAttestations(),
@@ -573,10 +795,45 @@ export class SequencerPublisher {
573
795
  const round = await base.computeRound(slotNumber);
574
796
  const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
575
797
 
798
+ if (roundInfo.quorumReached) {
799
+ return false;
800
+ }
801
+
576
802
  if (roundInfo.lastSignalSlot >= slotNumber) {
577
803
  return false;
578
804
  }
579
805
 
806
+ if (await this.isPayloadEmpty(payload)) {
807
+ this.log.warn(`Skipping vote cast for payload with empty code`);
808
+ return false;
809
+ }
810
+
811
+ // Check if payload was already submitted to governance
812
+ const cacheKey = payload.toString();
813
+ if (!this.payloadProposedCache.has(cacheKey)) {
814
+ try {
815
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
816
+ const proposed = await retry(
817
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
818
+ 'Check if payload was proposed',
819
+ makeBackoff([0, 1, 2]),
820
+ this.log,
821
+ true,
822
+ );
823
+ if (proposed) {
824
+ this.payloadProposedCache.add(cacheKey);
825
+ }
826
+ } catch (err) {
827
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
828
+ return false;
829
+ }
830
+ }
831
+
832
+ if (this.payloadProposedCache.has(cacheKey)) {
833
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
834
+ return false;
835
+ }
836
+
580
837
  const cachedLastVote = this.lastActions[signalType];
581
838
  this.lastActions[signalType] = slotNumber;
582
839
  const action = signalType;
@@ -595,11 +852,26 @@ export class SequencerPublisher {
595
852
  lastValidL2Slot: slotNumber,
596
853
  });
597
854
 
855
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
856
+
598
857
  try {
599
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
858
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
600
859
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
601
860
  } catch (err) {
602
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
861
+ const viemError = formatViemError(err);
862
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
863
+ this.backupFailedTx({
864
+ id: keccak256(request.data!),
865
+ failureType: 'simulation',
866
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
867
+ l1BlockNumber: l1BlockNumber.toString(),
868
+ error: { message: viemError.message, name: viemError.name },
869
+ context: {
870
+ actions: [action],
871
+ slot: slotNumber,
872
+ sender: this.getSenderAddress().toString(),
873
+ },
874
+ });
603
875
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
604
876
  }
605
877
 
@@ -619,14 +891,14 @@ export class SequencerPublisher {
619
891
  const logData = { ...result, slotNumber, round, payload: payload.toString() };
620
892
  if (!success) {
621
893
  this.log.error(
622
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} failed`,
894
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} failed`,
623
895
  logData,
624
896
  );
625
897
  this.lastActions[signalType] = cachedLastVote;
626
898
  return false;
627
899
  } else {
628
900
  this.log.info(
629
- `Signaling in [${action}] for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
901
+ `Signaling in ${action} for ${payload} at slot ${slotNumber} in round ${round} succeeded`,
630
902
  logData,
631
903
  );
632
904
  return true;
@@ -636,6 +908,17 @@ export class SequencerPublisher {
636
908
  return true;
637
909
  }
638
910
 
911
+ private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
912
+ const key = payload.toString();
913
+ const cached = this.isPayloadEmptyCache.get(key);
914
+ if (cached) {
915
+ return cached;
916
+ }
917
+ const isEmpty = !(await this.l1TxUtils.getCode(payload));
918
+ this.isPayloadEmptyCache.set(key, isEmpty);
919
+ return isEmpty;
920
+ }
921
+
639
922
  /**
640
923
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
641
924
  * @param slotNumber - The slot number to cast a signal for.
@@ -783,30 +1066,25 @@ export class SequencerPublisher {
783
1066
  return true;
784
1067
  }
785
1068
 
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,
1069
+ /** Simulates and enqueues a proposal for a checkpoint on L1 */
1070
+ public async enqueueProposeCheckpoint(
1071
+ checkpoint: Checkpoint,
794
1072
  attestationsAndSigners: CommitteeAttestationsAndSigners,
795
1073
  attestationsAndSignersSignature: Signature,
796
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
797
- ): Promise<boolean> {
798
- const checkpointHeader = block.getCheckpointHeader();
1074
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1075
+ ): Promise<void> {
1076
+ const checkpointHeader = checkpoint.header;
799
1077
 
800
- const blobFields = block.getCheckpointBlobFields();
801
- const blobs = getBlobsPerL1Block(blobFields);
1078
+ const blobFields = checkpoint.toBlobFields();
1079
+ const blobs = await getBlobsPerL1Block(blobFields);
802
1080
 
803
- const proposeTxArgs = {
1081
+ const proposeTxArgs: L1ProcessArgs = {
804
1082
  header: checkpointHeader,
805
- archive: block.archive.root.toBuffer(),
806
- body: block.body.toBuffer(),
1083
+ archive: checkpoint.archive.root.toBuffer(),
807
1084
  blobs,
808
1085
  attestationsAndSigners,
809
1086
  attestationsAndSignersSignature,
1087
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
810
1088
  };
811
1089
 
812
1090
  let ts: bigint;
@@ -817,22 +1095,29 @@ export class SequencerPublisher {
817
1095
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
818
1096
  // make time consistency checks break.
819
1097
  // 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);
1098
+ ts = await this.validateCheckpointForSubmission(
1099
+ checkpoint,
1100
+ attestationsAndSigners,
1101
+ attestationsAndSignersSignature,
1102
+ opts,
1103
+ );
821
1104
  } 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,
1105
+ this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1106
+ ...checkpoint.getStats(),
1107
+ slotNumber: checkpoint.header.slotNumber,
1108
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
826
1109
  });
827
1110
  throw err;
828
1111
  }
829
1112
 
830
- this.log.verbose(`Enqueuing block propose transaction`, { ...block.toBlockInfo(), ...opts });
831
- await this.addProposeTx(block, proposeTxArgs, opts, ts);
832
- return true;
1113
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1114
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
833
1115
  }
834
1116
 
835
- public enqueueInvalidateBlock(request: InvalidateBlockRequest | undefined, opts: { txTimeoutAt?: Date } = {}) {
1117
+ public enqueueInvalidateCheckpoint(
1118
+ request: InvalidateCheckpointRequest | undefined,
1119
+ opts: { txTimeoutAt?: Date } = {},
1120
+ ) {
836
1121
  if (!request) {
837
1122
  return;
838
1123
  }
@@ -840,9 +1125,9 @@ export class SequencerPublisher {
840
1125
  // We issued the simulation against the rollup contract, so we need to account for the overhead of the multicall3
841
1126
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(request.gasUsed) * 64) / 63)));
842
1127
 
843
- const { gasUsed, blockNumber } = request;
844
- const logData = { gasUsed, blockNumber, gasLimit, opts };
845
- this.log.verbose(`Enqueuing invalidate block request`, logData);
1128
+ const { gasUsed, checkpointNumber } = request;
1129
+ const logData = { gasUsed, checkpointNumber, gasLimit, opts };
1130
+ this.log.verbose(`Enqueuing invalidate checkpoint request`, logData);
846
1131
  this.addRequest({
847
1132
  action: `invalidate-by-${request.reason}`,
848
1133
  request: request.request,
@@ -855,9 +1140,9 @@ export class SequencerPublisher {
855
1140
  result.receipt.status === 'success' &&
856
1141
  tryExtractEvent(result.receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointInvalidated');
857
1142
  if (!success) {
858
- this.log.warn(`Invalidate block ${request.blockNumber} failed`, { ...result, ...logData });
1143
+ this.log.warn(`Invalidate checkpoint ${request.checkpointNumber} failed`, { ...result, ...logData });
859
1144
  } else {
860
- this.log.info(`Invalidate block ${request.blockNumber} succeeded`, { ...result, ...logData });
1145
+ this.log.info(`Invalidate checkpoint ${request.checkpointNumber} succeeded`, { ...result, ...logData });
861
1146
  }
862
1147
  return !!success;
863
1148
  },
@@ -882,13 +1167,30 @@ export class SequencerPublisher {
882
1167
 
883
1168
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
884
1169
 
1170
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1171
+
885
1172
  let gasUsed: bigint;
1173
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
886
1174
  try {
887
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1175
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
888
1176
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
889
1177
  } catch (err) {
890
- const viemError = formatViemError(err);
1178
+ const viemError = formatViemError(err, simulateAbi);
891
1179
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1180
+
1181
+ this.backupFailedTx({
1182
+ id: keccak256(request.data!),
1183
+ failureType: 'simulation',
1184
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1185
+ l1BlockNumber: l1BlockNumber.toString(),
1186
+ error: { message: viemError.message, name: viemError.name },
1187
+ context: {
1188
+ actions: [action],
1189
+ slot: slotNumber,
1190
+ sender: this.getSenderAddress().toString(),
1191
+ },
1192
+ });
1193
+
892
1194
  return false;
893
1195
  }
894
1196
 
@@ -896,10 +1198,14 @@ export class SequencerPublisher {
896
1198
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
897
1199
  logData.gasLimit = gasLimit;
898
1200
 
1201
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1202
+ // when the tx is sent and a revert is diagnosed via simulation.
1203
+ const requestWithAbi = { ...request, abi: simulateAbi };
1204
+
899
1205
  this.log.debug(`Enqueuing ${action}`, logData);
900
1206
  this.addRequest({
901
1207
  action,
902
- request,
1208
+ request: requestWithAbi,
903
1209
  gasConfig: { gasLimit },
904
1210
  lastValidL2Slot: slotNumber,
905
1211
  checkSuccess: (_req, result) => {
@@ -936,7 +1242,7 @@ export class SequencerPublisher {
936
1242
  private async prepareProposeTx(
937
1243
  encodedData: L1ProcessArgs,
938
1244
  timestamp: bigint,
939
- options: { forcePendingBlockNumber?: number },
1245
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
940
1246
  ) {
941
1247
  const kzg = Blob.getViemKzgInstance();
942
1248
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
@@ -968,9 +1274,27 @@ export class SequencerPublisher {
968
1274
  kzg,
969
1275
  },
970
1276
  )
971
- .catch(err => {
972
- const { message, metaMessages } = formatViemError(err);
973
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1277
+ .catch(async err => {
1278
+ const viemError = formatViemError(err);
1279
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1280
+ const validateBlobsData = encodeFunctionData({
1281
+ abi: RollupAbi,
1282
+ functionName: 'validateBlobs',
1283
+ args: [blobInput],
1284
+ });
1285
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1286
+ this.backupFailedTx({
1287
+ id: keccak256(validateBlobsData),
1288
+ failureType: 'simulation',
1289
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1290
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1291
+ l1BlockNumber: l1BlockNumber.toString(),
1292
+ error: { message: viemError.message, name: viemError.name },
1293
+ context: {
1294
+ actions: ['validate-blobs'],
1295
+ sender: this.getSenderAddress().toString(),
1296
+ },
1297
+ });
974
1298
  throw new Error('Failed to validate blobs');
975
1299
  });
976
1300
  }
@@ -981,8 +1305,7 @@ export class SequencerPublisher {
981
1305
  header: encodedData.header.toViem(),
982
1306
  archive: toHex(encodedData.archive),
983
1307
  oracleInput: {
984
- // We are currently not modifying these. See #9963
985
- feeAssetPriceModifier: 0n,
1308
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
986
1309
  },
987
1310
  },
988
1311
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1008,7 +1331,7 @@ export class SequencerPublisher {
1008
1331
  readonly header: ViemHeader;
1009
1332
  readonly archive: `0x${string}`;
1010
1333
  readonly oracleInput: {
1011
- readonly feeAssetPriceModifier: 0n;
1334
+ readonly feeAssetPriceModifier: bigint;
1012
1335
  };
1013
1336
  },
1014
1337
  ViemCommitteeAttestations,
@@ -1017,7 +1340,7 @@ export class SequencerPublisher {
1017
1340
  `0x${string}`,
1018
1341
  ],
1019
1342
  timestamp: bigint,
1020
- options: { forcePendingBlockNumber?: number },
1343
+ options: { forcePendingCheckpointNumber?: CheckpointNumber },
1021
1344
  ) {
1022
1345
  const rollupData = encodeFunctionData({
1023
1346
  abi: RollupAbi,
@@ -1025,10 +1348,10 @@ export class SequencerPublisher {
1025
1348
  args,
1026
1349
  });
1027
1350
 
1028
- // override the pending block number if requested
1029
- const forcePendingBlockNumberStateDiff = (
1030
- options.forcePendingBlockNumber !== undefined
1031
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingBlockNumber)
1351
+ // override the pending checkpoint number if requested
1352
+ const forcePendingCheckpointNumberStateDiff = (
1353
+ options.forcePendingCheckpointNumber !== undefined
1354
+ ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1032
1355
  : []
1033
1356
  ).flatMap(override => override.stateDiff ?? []);
1034
1357
 
@@ -1038,7 +1361,7 @@ export class SequencerPublisher {
1038
1361
  // @note we override checkBlob to false since blobs are not part simulate()
1039
1362
  stateDiff: [
1040
1363
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1041
- ...forcePendingBlockNumberStateDiff,
1364
+ ...forcePendingCheckpointNumberStateDiff,
1042
1365
  ],
1043
1366
  },
1044
1367
  ];
@@ -1050,25 +1373,27 @@ export class SequencerPublisher {
1050
1373
  });
1051
1374
  }
1052
1375
 
1376
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1377
+
1053
1378
  const simulationResult = await this.l1TxUtils
1054
1379
  .simulate(
1055
1380
  {
1056
1381
  to: this.rollupContract.address,
1057
1382
  data: rollupData,
1058
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1383
+ gas: MAX_L1_TX_LIMIT,
1059
1384
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1060
1385
  },
1061
1386
  {
1062
1387
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1063
1388
  time: timestamp + 1n,
1064
1389
  // @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,
1390
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1066
1391
  },
1067
1392
  stateOverrides,
1068
1393
  RollupAbi,
1069
1394
  {
1070
1395
  // @note fallback gas estimate to use if the node doesn't support simulation API
1071
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1396
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1072
1397
  },
1073
1398
  )
1074
1399
  .catch(err => {
@@ -1078,11 +1403,23 @@ export class SequencerPublisher {
1078
1403
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1079
1404
  // Return a minimal simulation result with the fallback gas estimate
1080
1405
  return {
1081
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1406
+ gasUsed: MAX_L1_TX_LIMIT,
1082
1407
  logs: [],
1083
1408
  };
1084
1409
  }
1085
1410
  this.log.error(`Failed to simulate propose tx`, viemError);
1411
+ this.backupFailedTx({
1412
+ id: keccak256(rollupData),
1413
+ failureType: 'simulation',
1414
+ request: { to: this.rollupContract.address, data: rollupData },
1415
+ l1BlockNumber: l1BlockNumber.toString(),
1416
+ error: { message: viemError.message, name: viemError.name },
1417
+ context: {
1418
+ actions: ['propose'],
1419
+ slot: Number(args[0].header.slotNumber),
1420
+ sender: this.getSenderAddress().toString(),
1421
+ },
1422
+ });
1086
1423
  throw err;
1087
1424
  });
1088
1425
 
@@ -1090,11 +1427,12 @@ export class SequencerPublisher {
1090
1427
  }
1091
1428
 
1092
1429
  private async addProposeTx(
1093
- block: L2Block,
1430
+ checkpoint: Checkpoint,
1094
1431
  encodedData: L1ProcessArgs,
1095
- opts: { txTimeoutAt?: Date; forcePendingBlockNumber?: number } = {},
1432
+ opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1096
1433
  timestamp: bigint,
1097
1434
  ): Promise<void> {
1435
+ const slot = checkpoint.header.slotNumber;
1098
1436
  const timer = new Timer();
1099
1437
  const kzg = Blob.getViemKzgInstance();
1100
1438
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
@@ -1109,11 +1447,13 @@ export class SequencerPublisher {
1109
1447
  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
1448
  );
1111
1449
 
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
- });
1450
+ // Send the blobs to the blob client preemptively. This helps in tests where the sequencer mistakingly thinks that the propose
1451
+ // tx fails but it does get mined. We make sure that the blobs are sent to the blob client regardless of the tx outcome.
1452
+ void Promise.resolve().then(() =>
1453
+ this.blobClient.sendBlobsToFilestore(encodedData.blobs).catch(_err => {
1454
+ this.log.error('Failed to send blobs to blob client');
1455
+ }),
1456
+ );
1117
1457
 
1118
1458
  return this.addRequest({
1119
1459
  action: 'propose',
@@ -1121,7 +1461,7 @@ export class SequencerPublisher {
1121
1461
  to: this.rollupContract.address,
1122
1462
  data: rollupData,
1123
1463
  },
1124
- lastValidL2Slot: block.header.globalVariables.slotNumber,
1464
+ lastValidL2Slot: checkpoint.header.slotNumber,
1125
1465
  gasConfig: { ...opts, gasLimit },
1126
1466
  blobConfig: {
1127
1467
  blobs: encodedData.blobs.map(b => b.data),
@@ -1136,11 +1476,12 @@ export class SequencerPublisher {
1136
1476
  receipt &&
1137
1477
  receipt.status === 'success' &&
1138
1478
  tryExtractEvent(receipt.logs, this.rollupContract.address, RollupAbi, 'CheckpointProposed');
1479
+
1139
1480
  if (success) {
1140
1481
  const endBlock = receipt.blockNumber;
1141
1482
  const inclusionBlocks = Number(endBlock - startBlock);
1142
1483
  const { calldataGas, calldataSize, sender } = stats!;
1143
- const publishStats: L1PublishBlockStats = {
1484
+ const publishStats: L1PublishCheckpointStats = {
1144
1485
  gasPrice: receipt.effectiveGasPrice,
1145
1486
  gasUsed: receipt.gasUsed,
1146
1487
  blobGasUsed: receipt.blobGasUsed ?? 0n,
@@ -1149,23 +1490,26 @@ export class SequencerPublisher {
1149
1490
  calldataGas,
1150
1491
  calldataSize,
1151
1492
  sender,
1152
- ...block.getStats(),
1493
+ ...checkpoint.getStats(),
1153
1494
  eventName: 'rollup-published-to-l1',
1154
1495
  blobCount: encodedData.blobs.length,
1155
1496
  inclusionBlocks,
1156
1497
  };
1157
- this.log.info(`Published L2 block to L1 rollup contract`, { ...stats, ...block.getStats(), ...receipt });
1498
+ this.log.info(`Published checkpoint ${checkpoint.number} at slot ${slot} to rollup contract`, {
1499
+ ...stats,
1500
+ ...checkpoint.getStats(),
1501
+ ...pick(receipt, 'transactionHash', 'blockHash'),
1502
+ });
1158
1503
  this.metrics.recordProcessBlockTx(timer.ms(), publishStats);
1159
1504
 
1160
1505
  return true;
1161
1506
  } else {
1162
1507
  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
- });
1508
+ this.log.error(
1509
+ `Publishing checkpoint at slot ${slot} failed with ${errorMsg ?? 'no error message'}`,
1510
+ undefined,
1511
+ { ...checkpoint.getStats(), ...receipt },
1512
+ );
1169
1513
  return false;
1170
1514
  }
1171
1515
  },