@aztec/sequencer-client 0.0.1-commit.5daedc8 → 0.0.1-commit.5de5ca79e

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