@aztec/sequencer-client 0.0.1-commit.b655e406 → 0.0.1-commit.c0b82b2

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