@aztec/sequencer-client 0.0.1-commit.fce3e4f → 0.0.1-commit.ffe5b04ea

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