@aztec/sequencer-client 0.0.1-commit.0c875d939 → 0.0.1-commit.0ec55a70b

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 (103) hide show
  1. package/dest/client/sequencer-client.d.ts +17 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +63 -30
  4. package/dest/config.d.ts +27 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +55 -30
  7. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  9. package/dest/global_variable_builder/fee_predictor.js +128 -0
  10. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  12. package/dest/global_variable_builder/fee_provider.js +58 -0
  13. package/dest/global_variable_builder/global_builder.d.ts +15 -16
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +16 -50
  16. package/dest/global_variable_builder/index.d.ts +4 -2
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/publisher/config.d.ts +47 -17
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +121 -42
  22. package/dest/publisher/index.d.ts +2 -1
  23. package/dest/publisher/index.d.ts.map +1 -1
  24. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  25. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
  28. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  29. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  30. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  31. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  32. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  33. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  34. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  35. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  36. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
  37. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  38. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  39. package/dest/publisher/sequencer-publisher.d.ts +68 -48
  40. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher.js +421 -137
  42. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  43. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  44. package/dest/sequencer/chain_state_overrides.js +39 -0
  45. package/dest/sequencer/checkpoint_proposal_job.d.ts +46 -10
  46. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  47. package/dest/sequencer/checkpoint_proposal_job.js +700 -220
  48. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  49. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  50. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  51. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  52. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  53. package/dest/sequencer/checkpoint_voter.js +2 -5
  54. package/dest/sequencer/events.d.ts +7 -1
  55. package/dest/sequencer/events.d.ts.map +1 -1
  56. package/dest/sequencer/metrics.d.ts +20 -8
  57. package/dest/sequencer/metrics.d.ts.map +1 -1
  58. package/dest/sequencer/metrics.js +92 -21
  59. package/dest/sequencer/sequencer.d.ts +53 -19
  60. package/dest/sequencer/sequencer.d.ts.map +1 -1
  61. package/dest/sequencer/sequencer.js +188 -102
  62. package/dest/sequencer/timetable.d.ts +17 -3
  63. package/dest/sequencer/timetable.d.ts.map +1 -1
  64. package/dest/sequencer/timetable.js +51 -43
  65. package/dest/sequencer/types.d.ts +2 -2
  66. package/dest/sequencer/types.d.ts.map +1 -1
  67. package/dest/test/index.d.ts +3 -5
  68. package/dest/test/index.d.ts.map +1 -1
  69. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  70. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  71. package/dest/test/mock_checkpoint_builder.js +39 -30
  72. package/dest/test/utils.d.ts +1 -1
  73. package/dest/test/utils.d.ts.map +1 -1
  74. package/dest/test/utils.js +7 -6
  75. package/package.json +27 -28
  76. package/src/client/sequencer-client.ts +81 -30
  77. package/src/config.ts +68 -35
  78. package/src/global_variable_builder/README.md +44 -0
  79. package/src/global_variable_builder/fee_predictor.ts +172 -0
  80. package/src/global_variable_builder/fee_provider.ts +75 -0
  81. package/src/global_variable_builder/global_builder.ts +27 -63
  82. package/src/global_variable_builder/index.ts +3 -1
  83. package/src/publisher/config.ts +157 -45
  84. package/src/publisher/index.ts +3 -0
  85. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  86. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
  87. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  88. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  89. package/src/publisher/sequencer-publisher-factory.ts +38 -9
  90. package/src/publisher/sequencer-publisher.ts +474 -197
  91. package/src/sequencer/README.md +82 -13
  92. package/src/sequencer/chain_state_overrides.ts +87 -0
  93. package/src/sequencer/checkpoint_proposal_job.ts +866 -241
  94. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  95. package/src/sequencer/checkpoint_voter.ts +1 -12
  96. package/src/sequencer/events.ts +6 -1
  97. package/src/sequencer/metrics.ts +108 -25
  98. package/src/sequencer/sequencer.ts +262 -116
  99. package/src/sequencer/timetable.ts +64 -52
  100. package/src/sequencer/types.ts +1 -1
  101. package/src/test/index.ts +2 -4
  102. package/src/test/mock_checkpoint_builder.ts +51 -48
  103. package/src/test/utils.ts +28 -10
@@ -3,52 +3,80 @@ import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
- type EmpireSlashingProposerContract,
7
6
  FeeAssetPriceOracle,
8
7
  type GovernanceProposerContract,
9
8
  type IEmpireBase,
10
9
  MULTI_CALL_3_ADDRESS,
11
10
  Multicall3,
12
11
  RollupContract,
13
- type TallySlashingProposerContract,
12
+ SimulationOverridesBuilder,
13
+ type SimulationOverridesPlan,
14
+ type SlashingProposerContract,
14
15
  type ViemCommitteeAttestations,
15
16
  type ViemHeader,
17
+ buildSimulationOverridesStateOverride,
16
18
  } from '@aztec/ethereum/contracts';
17
19
  import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
18
20
  import {
19
21
  type L1BlobInputs,
20
22
  type L1TxConfig,
21
23
  type L1TxRequest,
24
+ type L1TxUtils,
22
25
  MAX_L1_TX_LIMIT,
23
26
  type TransactionStats,
24
27
  WEI_CONST,
25
28
  } from '@aztec/ethereum/l1-tx-utils';
26
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
27
29
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
28
30
  import { sumBigint } from '@aztec/foundation/bigint';
29
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
30
31
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
32
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
31
33
  import { pick } from '@aztec/foundation/collection';
32
34
  import type { Fr } from '@aztec/foundation/curves/bn254';
35
+ import { TimeoutError } from '@aztec/foundation/error';
33
36
  import { EthAddress } from '@aztec/foundation/eth-address';
34
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
39
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
36
41
  import { bufferToHex } from '@aztec/foundation/string';
37
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
38
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
39
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
40
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
41
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
42
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
43
48
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
44
49
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
45
50
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
46
51
 
47
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
48
-
49
- import type { PublisherConfig, TxSenderConfig } from './config.js';
52
+ import {
53
+ type Hex,
54
+ type TransactionReceipt,
55
+ type TypedDataDefinition,
56
+ encodeFunctionData,
57
+ keccak256,
58
+ multicall3Abi,
59
+ toHex,
60
+ } from 'viem';
61
+
62
+ import type { SequencerPublisherConfig } from './config.js';
63
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
50
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
51
65
 
66
+ /** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
67
+ export type SendRequestsResult = {
68
+ /** The L1 transaction receipt or error from the bundled multicall. */
69
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
70
+ /** Actions that expired (past their deadline) before the request was sent. */
71
+ expiredActions: Action[];
72
+ /** Actions that were included in the sent L1 transaction. */
73
+ sentActions: Action[];
74
+ /** Actions whose L1 simulation succeeded (subset of sentActions). */
75
+ successfulActions: Action[];
76
+ /** Actions whose L1 simulation failed (subset of sentActions). */
77
+ failedActions: Action[];
78
+ };
79
+
52
80
  /** Arguments to the process method of the rollup contract */
53
81
  type L1ProcessArgs = {
54
82
  /** The L2 block header. */
@@ -70,16 +98,13 @@ export const Actions = [
70
98
  'invalidate-by-insufficient-attestations',
71
99
  'propose',
72
100
  'governance-signal',
73
- 'empire-slashing-signal',
74
- 'create-empire-payload',
75
- 'execute-empire-payload',
76
101
  'vote-offenses',
77
102
  'execute-slash',
78
103
  ] as const;
79
104
 
80
105
  export type Action = (typeof Actions)[number];
81
106
 
82
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
83
108
 
84
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
85
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -90,6 +115,13 @@ export type InvalidateCheckpointRequest = {
90
115
  gasUsed: bigint;
91
116
  checkpointNumber: CheckpointNumber;
92
117
  forcePendingCheckpointNumber: CheckpointNumber;
118
+ /** Archive at the rollback target checkpoint (checkpoint N-1). */
119
+ lastArchive: Fr;
120
+ };
121
+
122
+ type EnqueueProposeCheckpointOpts = {
123
+ txTimeoutAt?: Date;
124
+ simulationOverridesPlan?: SimulationOverridesPlan;
93
125
  };
94
126
 
95
127
  interface RequestWithExpiry {
@@ -98,6 +130,8 @@ interface RequestWithExpiry {
98
130
  lastValidL2Slot: SlotNumber;
99
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
100
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
101
135
  checkSuccess: (
102
136
  request: L1TxRequest,
103
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -108,6 +142,7 @@ export class SequencerPublisher {
108
142
  private interrupted = false;
109
143
  private metrics: SequencerPublisherMetrics;
110
144
  public epochCache: EpochCache;
145
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
111
146
 
112
147
  protected governanceLog = createLogger('sequencer:publisher:governance');
113
148
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -115,65 +150,80 @@ export class SequencerPublisher {
115
150
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
116
151
 
117
152
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
153
+ private payloadProposedCache: Set<string> = new Set<string>();
118
154
 
119
155
  protected log: Logger;
120
156
  protected ethereumSlotDuration: bigint;
157
+ protected aztecSlotDuration: bigint;
158
+
159
+ /** Date provider for wall-clock time. */
160
+ private readonly dateProvider: DateProvider;
121
161
 
122
162
  private blobClient: BlobClientInterface;
123
163
 
124
164
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
125
165
  private proposerAddressForSimulation?: EthAddress;
126
166
 
167
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
168
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
169
+
127
170
  /** L1 fee analyzer for fisherman mode */
128
171
  private l1FeeAnalyzer?: L1FeeAnalyzer;
129
172
 
130
173
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
131
174
  private feeAssetPriceOracle: FeeAssetPriceOracle;
132
175
 
176
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
177
+ private readonly interruptibleSleep = new InterruptibleSleep();
178
+
133
179
  // A CALL to a cold address is 2700 gas
134
180
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
135
181
 
136
182
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
137
183
  public static VOTE_GAS_GUESS: bigint = 800_000n;
138
184
 
139
- public l1TxUtils: L1TxUtilsWithBlobs;
185
+ public l1TxUtils: L1TxUtils;
140
186
  public rollupContract: RollupContract;
141
187
  public govProposerContract: GovernanceProposerContract;
142
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
143
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
144
189
 
145
190
  public readonly tracer: Tracer;
146
191
 
147
192
  protected requests: RequestWithExpiry[] = [];
148
193
 
149
194
  constructor(
150
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
195
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
196
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
151
197
  deps: {
152
198
  telemetry?: TelemetryClient;
153
199
  blobClient: BlobClientInterface;
154
- l1TxUtils: L1TxUtilsWithBlobs;
200
+ l1TxUtils: L1TxUtils;
155
201
  rollupContract: RollupContract;
156
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
157
203
  governanceProposerContract: GovernanceProposerContract;
158
- slashFactoryContract: SlashFactoryContract;
159
204
  epochCache: EpochCache;
160
205
  dateProvider: DateProvider;
161
206
  metrics: SequencerPublisherMetrics;
162
207
  lastActions: Partial<Record<Action, SlotNumber>>;
163
208
  log?: Logger;
209
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
164
210
  },
165
211
  ) {
166
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
167
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
214
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
168
216
  this.epochCache = deps.epochCache;
169
217
  this.lastActions = deps.lastActions;
170
218
 
171
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
172
221
 
173
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
174
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
175
224
  this.tracer = telemetry.getTracer('SequencerPublisher');
176
225
  this.l1TxUtils = deps.l1TxUtils;
226
+ this.getNextPublisher = deps.getNextPublisher;
177
227
 
178
228
  this.rollupContract = deps.rollupContract;
179
229
 
@@ -185,8 +235,6 @@ export class SequencerPublisher {
185
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
186
236
  this.slashingProposerContract = newSlashingProposer;
187
237
  });
188
- this.slashFactoryContract = deps.slashFactoryContract;
189
-
190
238
  // Initialize L1 fee analyzer for fisherman mode
191
239
  if (config.fishermanMode) {
192
240
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -202,6 +250,31 @@ export class SequencerPublisher {
202
250
  this.rollupContract,
203
251
  createLogger('sequencer:publisher:price-oracle'),
204
252
  );
253
+
254
+ // Initialize failed L1 tx store (optional, for test networks)
255
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
256
+ }
257
+
258
+ /**
259
+ * Backs up a failed L1 transaction to the configured store for debugging.
260
+ * Does nothing if no store is configured.
261
+ */
262
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
263
+ if (!this.failedTxStore) {
264
+ return;
265
+ }
266
+
267
+ const tx: FailedL1Tx = {
268
+ ...failedTx,
269
+ timestamp: Date.now(),
270
+ };
271
+
272
+ // Fire and forget - don't block on backup
273
+ void this.failedTxStore
274
+ .then(store => store?.saveFailedTx(tx))
275
+ .catch(err => {
276
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
277
+ });
205
278
  }
206
279
 
207
280
  public getRollupContract(): RollupContract {
@@ -240,7 +313,7 @@ export class SequencerPublisher {
240
313
  }
241
314
 
242
315
  public getCurrentL2Slot(): SlotNumber {
243
- return this.epochCache.getEpochAndSlotNow().slot;
316
+ return this.epochCache.getSlotNow();
244
317
  }
245
318
 
246
319
  /**
@@ -318,9 +391,10 @@ export class SequencerPublisher {
318
391
  * - undefined if no valid requests are found OR the tx failed to send.
319
392
  */
320
393
  @trackSpan('SequencerPublisher.sendRequests')
321
- public async sendRequests() {
394
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
322
395
  const requestsToProcess = [...this.requests];
323
396
  this.requests = [];
397
+
324
398
  if (this.interrupted || requestsToProcess.length === 0) {
325
399
  return undefined;
326
400
  }
@@ -353,8 +427,8 @@ export class SequencerPublisher {
353
427
  // @note - we can only have one blob config per bundle
354
428
  // find requests with gas and blob configs
355
429
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
356
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
357
- const blobConfigs = requestsToProcess.filter(request => request.blobConfig).map(request => request.blobConfig);
430
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
431
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
358
432
 
359
433
  if (blobConfigs.length > 1) {
360
434
  throw new Error('Multiple blob configs found');
@@ -383,19 +457,36 @@ export class SequencerPublisher {
383
457
  validRequests.sort((a, b) => compareActions(a.action, b.action));
384
458
 
385
459
  try {
460
+ // Capture context for failed tx backup before sending
461
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
462
+ const multicallData = encodeFunctionData({
463
+ abi: multicall3Abi,
464
+ functionName: 'aggregate3',
465
+ args: [
466
+ validRequests.map(r => ({
467
+ target: r.request.to!,
468
+ callData: r.request.data!,
469
+ allowFailure: true,
470
+ })),
471
+ ],
472
+ });
473
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
474
+
475
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
476
+
386
477
  this.log.debug('Forwarding transactions', {
387
478
  validRequests: validRequests.map(request => request.action),
388
479
  txConfig,
389
480
  });
390
- const result = await Multicall3.forward(
391
- validRequests.map(request => request.request),
392
- this.l1TxUtils,
393
- txConfig,
394
- blobConfig,
395
- this.rollupContract.address,
396
- this.log,
481
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
482
+ if (result === undefined) {
483
+ return undefined;
484
+ }
485
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
486
+ validRequests,
487
+ result,
488
+ txContext,
397
489
  );
398
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
399
490
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
400
491
  } catch (err) {
401
492
  const viemError = formatViemError(err);
@@ -413,16 +504,127 @@ export class SequencerPublisher {
413
504
  }
414
505
  }
415
506
 
507
+ /**
508
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
509
+ * failure occurs (i.e. the tx never reached the chain).
510
+ * On-chain reverts and simulation errors are returned as-is without rotation.
511
+ */
512
+ private async forwardWithPublisherRotation(
513
+ validRequests: RequestWithExpiry[],
514
+ txConfig: RequestWithExpiry['gasConfig'],
515
+ blobConfig: L1BlobInputs | undefined,
516
+ ) {
517
+ const triedAddresses: EthAddress[] = [];
518
+ let currentPublisher = this.l1TxUtils;
519
+
520
+ while (true) {
521
+ triedAddresses.push(currentPublisher.getSenderAddress());
522
+ try {
523
+ const result = await Multicall3.forward(
524
+ validRequests.map(r => r.request),
525
+ currentPublisher,
526
+ txConfig,
527
+ blobConfig,
528
+ this.rollupContract.address,
529
+ this.log,
530
+ );
531
+ this.l1TxUtils = currentPublisher;
532
+ return result;
533
+ } catch (err) {
534
+ if (err instanceof TimeoutError) {
535
+ throw err;
536
+ }
537
+ const viemError = formatViemError(err);
538
+ if (!this.getNextPublisher) {
539
+ this.log.error('Failed to publish bundled transactions', viemError);
540
+ return undefined;
541
+ }
542
+ this.log.warn(
543
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
544
+ viemError,
545
+ );
546
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
547
+ if (!nextPublisher) {
548
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
549
+ return undefined;
550
+ }
551
+ currentPublisher = nextPublisher;
552
+ }
553
+ }
554
+ }
555
+
556
+ /*
557
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
558
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
559
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
560
+ */
561
+ public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
562
+ const ms = submitAfter.getTime() - this.dateProvider.now();
563
+ if (ms > 0) {
564
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
565
+ await this.interruptibleSleep.sleep(ms);
566
+ }
567
+ if (this.interrupted) {
568
+ return undefined;
569
+ }
570
+
571
+ // Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
572
+ const validRequests: RequestWithExpiry[] = [];
573
+ for (const request of this.requests) {
574
+ if (!request.preCheck) {
575
+ validRequests.push(request);
576
+ continue;
577
+ }
578
+
579
+ try {
580
+ await request.preCheck();
581
+ validRequests.push(request);
582
+ } catch (err) {
583
+ this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
584
+ }
585
+ }
586
+
587
+ this.requests = validRequests;
588
+ if (this.requests.length === 0) {
589
+ return undefined;
590
+ }
591
+
592
+ return this.sendRequests();
593
+ }
594
+
416
595
  private callbackBundledTransactions(
417
596
  requests: RequestWithExpiry[],
418
- result?: { receipt: TransactionReceipt } | FormattedViemError,
597
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
598
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
419
599
  ) {
420
600
  const actionsListStr = requests.map(r => r.action).join(', ');
421
601
  if (result instanceof FormattedViemError) {
422
602
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
603
+ this.backupFailedTx({
604
+ id: keccak256(txContext.multicallData),
605
+ failureType: 'send-error',
606
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
607
+ blobData: txContext.blobData,
608
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
609
+ error: { message: result.message, name: result.name },
610
+ context: {
611
+ actions: requests.map(r => r.action),
612
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
613
+ sender: this.getSenderAddress().toString(),
614
+ },
615
+ });
423
616
  return { failedActions: requests.map(r => r.action) };
424
617
  } else {
425
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
618
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
619
+ result,
620
+ requests: requests.map(r => ({
621
+ ...r,
622
+ // Avoid logging large blob data
623
+ blobConfig: r.blobConfig
624
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
625
+ : undefined,
626
+ })),
627
+ });
426
628
  const successfulActions: Action[] = [];
427
629
  const failedActions: Action[] = [];
428
630
  for (const request of requests) {
@@ -432,27 +634,54 @@ export class SequencerPublisher {
432
634
  failedActions.push(request.action);
433
635
  }
434
636
  }
637
+ // Single backup for the whole reverted tx
638
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
639
+ this.backupFailedTx({
640
+ id: result.receipt.transactionHash,
641
+ failureType: 'revert',
642
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
643
+ blobData: txContext.blobData,
644
+ l1BlockNumber: result.receipt.blockNumber.toString(),
645
+ receipt: {
646
+ transactionHash: result.receipt.transactionHash,
647
+ blockNumber: result.receipt.blockNumber.toString(),
648
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
649
+ status: 'reverted',
650
+ },
651
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
652
+ context: {
653
+ actions: failedActions,
654
+ requests: requests
655
+ .filter(r => failedActions.includes(r.action))
656
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
657
+ sender: this.getSenderAddress().toString(),
658
+ },
659
+ });
660
+ }
435
661
  return { successfulActions, failedActions };
436
662
  }
437
663
  }
438
664
 
439
665
  /**
440
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
666
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
441
667
  * @param tipArchive - The archive to check
442
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
443
669
  */
444
- public canProposeAtNextEthBlock(
445
- tipArchive: Fr,
446
- msgSender: EthAddress,
447
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
448
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
449
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
450
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
451
673
 
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
675
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
676
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
677
+
452
678
  return this.rollupContract
453
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
454
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
455
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
456
685
  .catch(err => {
457
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
458
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -464,6 +693,7 @@ export class SequencerPublisher {
464
693
  return undefined;
465
694
  });
466
695
  }
696
+
467
697
  /**
468
698
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
469
699
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -473,13 +703,13 @@ export class SequencerPublisher {
473
703
  @trackSpan('SequencerPublisher.validateBlockHeader')
474
704
  public async validateBlockHeader(
475
705
  header: CheckpointHeader,
476
- opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
477
707
  ): Promise<void> {
478
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
479
709
 
480
710
  const args = [
481
711
  header.toViem(),
482
- CommitteeAttestationsAndSigners.empty().getPackedAttestations(),
712
+ CommitteeAttestationsAndSigners.packAttestations([]),
483
713
  [], // no signers
484
714
  Signature.empty().toViemSignature(),
485
715
  `0x${'0'.repeat(64)}`, // 32 empty bytes
@@ -487,10 +717,8 @@ export class SequencerPublisher {
487
717
  flags,
488
718
  ] as const;
489
719
 
490
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
491
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
492
- opts?.forcePendingCheckpointNumber,
493
- );
720
+ const ts = this.getSimulationTimestamp(header.slotNumber);
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
494
722
  let balance = 0n;
495
723
  if (this.config.fishermanMode) {
496
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -510,7 +738,7 @@ export class SequencerPublisher {
510
738
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
511
739
  from: MULTI_CALL_3_ADDRESS,
512
740
  },
513
- { time: ts + 1n },
741
+ { time: ts },
514
742
  stateOverrides,
515
743
  );
516
744
  this.log.debug(`Simulated validateHeader`);
@@ -543,6 +771,8 @@ export class SequencerPublisher {
543
771
  const request = this.buildInvalidateCheckpointRequest(validationResult);
544
772
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
545
773
 
774
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
775
+
546
776
  try {
547
777
  const { gasUsed } = await this.l1TxUtils.simulate(
548
778
  request,
@@ -561,6 +791,7 @@ export class SequencerPublisher {
561
791
  gasUsed,
562
792
  checkpointNumber,
563
793
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
794
+ lastArchive: validationResult.checkpoint.lastArchive,
564
795
  reason,
565
796
  };
566
797
  } catch (err) {
@@ -573,8 +804,8 @@ export class SequencerPublisher {
573
804
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
574
805
  { ...logData, request, error: viemError.message },
575
806
  );
576
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
577
- if (latestPendingCheckpointNumber < checkpointNumber) {
807
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
808
+ if (latestProposedCheckpointNumber < checkpointNumber) {
578
809
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
579
810
  return undefined;
580
811
  } else {
@@ -594,6 +825,18 @@ export class SequencerPublisher {
594
825
 
595
826
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
596
827
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
828
+ this.backupFailedTx({
829
+ id: keccak256(request.data!),
830
+ failureType: 'simulation',
831
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
832
+ l1BlockNumber: l1BlockNumber.toString(),
833
+ error: { message: viemError.message, name: viemError.name },
834
+ context: {
835
+ actions: [`invalidate-${reason}`],
836
+ checkpointNumber,
837
+ sender: this.getSenderAddress().toString(),
838
+ },
839
+ });
597
840
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
598
841
  }
599
842
  }
@@ -607,9 +850,7 @@ export class SequencerPublisher {
607
850
  const logData = { ...checkpoint, reason };
608
851
  this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
609
852
 
610
- const attestationsAndSigners = new CommitteeAttestationsAndSigners(
611
- validationResult.attestations,
612
- ).getPackedAttestations();
853
+ const attestationsAndSigners = CommitteeAttestationsAndSigners.packAttestations(validationResult.attestations);
613
854
 
614
855
  if (reason === 'invalid-attestation') {
615
856
  return this.rollupContract.buildInvalidateBadAttestationRequest(
@@ -636,27 +877,10 @@ export class SequencerPublisher {
636
877
  checkpoint: Checkpoint,
637
878
  attestationsAndSigners: CommitteeAttestationsAndSigners,
638
879
  attestationsAndSignersSignature: Signature,
639
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
640
- ): Promise<bigint> {
641
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
642
-
643
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
644
- // If we have no attestations, we still need to provide the empty attestations
645
- // so that the committee is recalculated correctly
646
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
647
- // if (ignoreSignatures) {
648
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
649
- // if (!committee) {
650
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
651
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
652
- // }
653
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
654
- // CommitteeAttestation.fromAddress(committeeMember),
655
- // );
656
- // }
657
-
880
+ simulationOverridesPlan?: SimulationOverridesPlan,
881
+ ): Promise<void> {
658
882
  const blobFields = checkpoint.toBlobFields();
659
- const blobs = getBlobsPerL1Block(blobFields);
883
+ const blobs = await getBlobsPerL1Block(blobFields);
660
884
  const blobInput = getPrefixedEthBlobCommitments(blobs);
661
885
 
662
886
  const args = [
@@ -673,13 +897,11 @@ export class SequencerPublisher {
673
897
  blobInput,
674
898
  ] as const;
675
899
 
676
- await this.simulateProposeTx(args, ts, options);
677
- return ts;
900
+ await this.simulateProposeTx(args, simulationOverridesPlan);
678
901
  }
679
902
 
680
903
  private async enqueueCastSignalHelper(
681
904
  slotNumber: SlotNumber,
682
- timestamp: bigint,
683
905
  signalType: GovernanceSignalAction,
684
906
  payload: EthAddress,
685
907
  base: IEmpireBase,
@@ -713,6 +935,32 @@ export class SequencerPublisher {
713
935
  return false;
714
936
  }
715
937
 
938
+ // Check if payload was already submitted to governance
939
+ const cacheKey = payload.toString();
940
+ if (!this.payloadProposedCache.has(cacheKey)) {
941
+ try {
942
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
943
+ const proposed = await retry(
944
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
945
+ 'Check if payload was proposed',
946
+ makeBackoff([0, 1, 2]),
947
+ this.log,
948
+ true,
949
+ );
950
+ if (proposed) {
951
+ this.payloadProposedCache.add(cacheKey);
952
+ }
953
+ } catch (err) {
954
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
955
+ return false;
956
+ }
957
+ }
958
+
959
+ if (this.payloadProposedCache.has(cacheKey)) {
960
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
961
+ return false;
962
+ }
963
+
716
964
  const cachedLastVote = this.lastActions[signalType];
717
965
  this.lastActions[signalType] = slotNumber;
718
966
  const action = signalType;
@@ -731,11 +979,30 @@ export class SequencerPublisher {
731
979
  lastValidL2Slot: slotNumber,
732
980
  });
733
981
 
982
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
983
+ const timestamp = this.getSimulationTimestamp(slotNumber);
984
+
734
985
  try {
735
986
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
736
987
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
737
988
  } catch (err) {
738
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
989
+ const viemError = formatViemError(err);
990
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
991
+ simulationTimestamp: timestamp,
992
+ l1BlockNumber,
993
+ });
994
+ this.backupFailedTx({
995
+ id: keccak256(request.data!),
996
+ failureType: 'simulation',
997
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
998
+ l1BlockNumber: l1BlockNumber.toString(),
999
+ error: { message: viemError.message, name: viemError.name },
1000
+ context: {
1001
+ actions: [action],
1002
+ slot: slotNumber,
1003
+ sender: this.getSenderAddress().toString(),
1004
+ },
1005
+ });
739
1006
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
740
1007
  }
741
1008
 
@@ -786,19 +1053,16 @@ export class SequencerPublisher {
786
1053
  /**
787
1054
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
788
1055
  * @param slotNumber - The slot number to cast a signal for.
789
- * @param timestamp - The timestamp of the slot to cast a signal for.
790
1056
  * @returns True if the signal was successfully enqueued, false otherwise.
791
1057
  */
792
1058
  public enqueueGovernanceCastSignal(
793
1059
  governancePayload: EthAddress,
794
1060
  slotNumber: SlotNumber,
795
- timestamp: bigint,
796
1061
  signerAddress: EthAddress,
797
1062
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
798
1063
  ): Promise<boolean> {
799
1064
  return this.enqueueCastSignalHelper(
800
1065
  slotNumber,
801
- timestamp,
802
1066
  'governance-signal',
803
1067
  governancePayload,
804
1068
  this.govProposerContract,
@@ -811,7 +1075,6 @@ export class SequencerPublisher {
811
1075
  public async enqueueSlashingActions(
812
1076
  actions: ProposerSlashAction[],
813
1077
  slotNumber: SlotNumber,
814
- timestamp: bigint,
815
1078
  signerAddress: EthAddress,
816
1079
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
817
1080
  ): Promise<boolean> {
@@ -822,58 +1085,6 @@ export class SequencerPublisher {
822
1085
 
823
1086
  for (const action of actions) {
824
1087
  switch (action.type) {
825
- case 'vote-empire-payload': {
826
- if (this.slashingProposerContract?.type !== 'empire') {
827
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
828
- break;
829
- }
830
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
831
- signerAddress,
832
- });
833
- await this.enqueueCastSignalHelper(
834
- slotNumber,
835
- timestamp,
836
- 'empire-slashing-signal',
837
- action.payload,
838
- this.slashingProposerContract,
839
- signerAddress,
840
- signer,
841
- );
842
- break;
843
- }
844
-
845
- case 'create-empire-payload': {
846
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
847
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
848
- await this.simulateAndEnqueueRequest(
849
- 'create-empire-payload',
850
- request,
851
- (receipt: TransactionReceipt) =>
852
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
853
- slotNumber,
854
- timestamp,
855
- );
856
- break;
857
- }
858
-
859
- case 'execute-empire-payload': {
860
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
861
- if (this.slashingProposerContract?.type !== 'empire') {
862
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
863
- return false;
864
- }
865
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
866
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
867
- await this.simulateAndEnqueueRequest(
868
- 'execute-empire-payload',
869
- request,
870
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
871
- slotNumber,
872
- timestamp,
873
- );
874
- break;
875
- }
876
-
877
1088
  case 'vote-offenses': {
878
1089
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
879
1090
  slotNumber,
@@ -881,19 +1092,17 @@ export class SequencerPublisher {
881
1092
  votesCount: action.votes.length,
882
1093
  signerAddress,
883
1094
  });
884
- if (this.slashingProposerContract?.type !== 'tally') {
885
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1095
+ if (!this.slashingProposerContract) {
1096
+ this.log.error('No slashing proposer contract available');
886
1097
  return false;
887
1098
  }
888
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
889
1099
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
890
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1100
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
891
1101
  await this.simulateAndEnqueueRequest(
892
1102
  'vote-offenses',
893
1103
  request,
894
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1104
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
895
1105
  slotNumber,
896
- timestamp,
897
1106
  );
898
1107
  break;
899
1108
  }
@@ -904,18 +1113,20 @@ export class SequencerPublisher {
904
1113
  round: action.round,
905
1114
  signerAddress,
906
1115
  });
907
- if (this.slashingProposerContract?.type !== 'tally') {
908
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1116
+ if (!this.slashingProposerContract) {
1117
+ this.log.error('No slashing proposer contract available');
909
1118
  return false;
910
1119
  }
911
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
912
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1120
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1121
+ action.round,
1122
+ action.committees,
1123
+ );
913
1124
  await this.simulateAndEnqueueRequest(
914
1125
  'execute-slash',
915
- request,
916
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1126
+ executeRequest,
1127
+ (receipt: TransactionReceipt) =>
1128
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
917
1129
  slotNumber,
918
- timestamp,
919
1130
  );
920
1131
  break;
921
1132
  }
@@ -935,12 +1146,12 @@ export class SequencerPublisher {
935
1146
  checkpoint: Checkpoint,
936
1147
  attestationsAndSigners: CommitteeAttestationsAndSigners,
937
1148
  attestationsAndSignersSignature: Signature,
938
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1149
+ opts: EnqueueProposeCheckpointOpts = {},
939
1150
  ): Promise<void> {
940
1151
  const checkpointHeader = checkpoint.header;
941
1152
 
942
1153
  const blobFields = checkpoint.toBlobFields();
943
- const blobs = getBlobsPerL1Block(blobFields);
1154
+ const blobs = await getBlobsPerL1Block(blobFields);
944
1155
 
945
1156
  const proposeTxArgs: L1ProcessArgs = {
946
1157
  header: checkpointHeader,
@@ -951,7 +1162,9 @@ export class SequencerPublisher {
951
1162
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
952
1163
  };
953
1164
 
954
- let ts: bigint;
1165
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1166
+ .withoutBlobCheck()
1167
+ .build();
955
1168
 
956
1169
  try {
957
1170
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
@@ -959,23 +1172,47 @@ export class SequencerPublisher {
959
1172
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
960
1173
  // make time consistency checks break.
961
1174
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
962
- ts = await this.validateCheckpointForSubmission(
1175
+ await this.validateCheckpointForSubmission(
963
1176
  checkpoint,
964
1177
  attestationsAndSigners,
965
1178
  attestationsAndSignersSignature,
966
- opts,
1179
+ simulationOverridesPlan,
967
1180
  );
968
1181
  } catch (err: any) {
969
1182
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
970
1183
  ...checkpoint.getStats(),
971
1184
  slotNumber: checkpoint.header.slotNumber,
972
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1185
+ simulationOverridesPlan,
973
1186
  });
974
1187
  throw err;
975
1188
  }
976
1189
 
977
- this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
978
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1190
+ // Build a pre-check callback that re-validates the checkpoint before L1 submission.
1191
+ // During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
1192
+ let preCheck = undefined;
1193
+ if (this.epochCache.isProposerPipeliningEnabled()) {
1194
+ preCheck = async () => {
1195
+ this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
1196
+ await this.validateCheckpointForSubmission(
1197
+ checkpoint,
1198
+ attestationsAndSigners,
1199
+ attestationsAndSignersSignature,
1200
+ simulationOverridesPlan,
1201
+ );
1202
+ };
1203
+ }
1204
+
1205
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1206
+ ...checkpoint.toCheckpointInfo(),
1207
+ txTimeoutAt: opts.txTimeoutAt,
1208
+ simulationOverridesPlan,
1209
+ });
1210
+ await this.addProposeTx(
1211
+ checkpoint,
1212
+ proposeTxArgs,
1213
+ { txTimeoutAt: opts.txTimeoutAt, simulationOverridesPlan },
1214
+ preCheck,
1215
+ );
979
1216
  }
980
1217
 
981
1218
  public enqueueInvalidateCheckpoint(
@@ -1018,8 +1255,8 @@ export class SequencerPublisher {
1018
1255
  request: L1TxRequest,
1019
1256
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
1020
1257
  slotNumber: SlotNumber,
1021
- timestamp: bigint,
1022
1258
  ) {
1259
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1023
1260
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1024
1261
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1025
1262
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1031,15 +1268,31 @@ export class SequencerPublisher {
1031
1268
 
1032
1269
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1033
1270
 
1271
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1272
+
1034
1273
  let gasUsed: bigint;
1035
1274
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1275
+
1036
1276
  try {
1037
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1277
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1038
1278
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1039
1279
  } catch (err) {
1040
1280
  const viemError = formatViemError(err, simulateAbi);
1041
1281
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1042
1282
 
1283
+ this.backupFailedTx({
1284
+ id: keccak256(request.data!),
1285
+ failureType: 'simulation',
1286
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1287
+ l1BlockNumber: l1BlockNumber.toString(),
1288
+ error: { message: viemError.message, name: viemError.name },
1289
+ context: {
1290
+ actions: [action],
1291
+ slot: slotNumber,
1292
+ sender: this.getSenderAddress().toString(),
1293
+ },
1294
+ });
1295
+
1043
1296
  return false;
1044
1297
  }
1045
1298
 
@@ -1079,6 +1332,7 @@ export class SequencerPublisher {
1079
1332
  */
1080
1333
  public interrupt() {
1081
1334
  this.interrupted = true;
1335
+ this.interruptibleSleep.interrupt();
1082
1336
  this.l1TxUtils.interrupt();
1083
1337
  }
1084
1338
 
@@ -1088,11 +1342,7 @@ export class SequencerPublisher {
1088
1342
  this.l1TxUtils.restart();
1089
1343
  }
1090
1344
 
1091
- private async prepareProposeTx(
1092
- encodedData: L1ProcessArgs,
1093
- timestamp: bigint,
1094
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1095
- ) {
1345
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
1096
1346
  const kzg = Blob.getViemKzgInstance();
1097
1347
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1098
1348
  this.log.debug('Validating blob input', { blobInput });
@@ -1123,9 +1373,27 @@ export class SequencerPublisher {
1123
1373
  kzg,
1124
1374
  },
1125
1375
  )
1126
- .catch(err => {
1127
- const { message, metaMessages } = formatViemError(err);
1128
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1376
+ .catch(async err => {
1377
+ const viemError = formatViemError(err);
1378
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1379
+ const validateBlobsData = encodeFunctionData({
1380
+ abi: RollupAbi,
1381
+ functionName: 'validateBlobs',
1382
+ args: [blobInput],
1383
+ });
1384
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1385
+ this.backupFailedTx({
1386
+ id: keccak256(validateBlobsData),
1387
+ failureType: 'simulation',
1388
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1389
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1390
+ l1BlockNumber: l1BlockNumber.toString(),
1391
+ error: { message: viemError.message, name: viemError.name },
1392
+ context: {
1393
+ actions: ['validate-blobs'],
1394
+ sender: this.getSenderAddress().toString(),
1395
+ },
1396
+ });
1129
1397
  throw new Error('Failed to validate blobs');
1130
1398
  });
1131
1399
  }
@@ -1145,7 +1413,7 @@ export class SequencerPublisher {
1145
1413
  blobInput,
1146
1414
  ] as const;
1147
1415
 
1148
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1416
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1149
1417
 
1150
1418
  return { args, blobEvaluationGas, rollupData, simulationResult };
1151
1419
  }
@@ -1153,7 +1421,6 @@ export class SequencerPublisher {
1153
1421
  /**
1154
1422
  * Simulates the propose tx with eth_simulateV1
1155
1423
  * @param args - The propose tx args
1156
- * @param timestamp - The timestamp to simulate proposal at
1157
1424
  * @returns The simulation result
1158
1425
  */
1159
1426
  private async simulateProposeTx(
@@ -1170,8 +1437,7 @@ export class SequencerPublisher {
1170
1437
  ViemSignature,
1171
1438
  `0x${string}`,
1172
1439
  ],
1173
- timestamp: bigint,
1174
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1440
+ simulationOverridesPlan?: SimulationOverridesPlan,
1175
1441
  ) {
1176
1442
  const rollupData = encodeFunctionData({
1177
1443
  abi: RollupAbi,
@@ -1179,23 +1445,7 @@ export class SequencerPublisher {
1179
1445
  args,
1180
1446
  });
1181
1447
 
1182
- // override the pending checkpoint number if requested
1183
- const forcePendingCheckpointNumberStateDiff = (
1184
- options.forcePendingCheckpointNumber !== undefined
1185
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1186
- : []
1187
- ).flatMap(override => override.stateDiff ?? []);
1188
-
1189
- const stateOverrides: StateOverride = [
1190
- {
1191
- address: this.rollupContract.address,
1192
- // @note we override checkBlob to false since blobs are not part simulate()
1193
- stateDiff: [
1194
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1195
- ...forcePendingCheckpointNumberStateDiff,
1196
- ],
1197
- },
1198
- ];
1448
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1199
1449
  // In fisherman mode, simulate as the proposer but with sufficient balance
1200
1450
  if (this.proposerAddressForSimulation) {
1201
1451
  stateOverrides.push({
@@ -1204,6 +1454,9 @@ export class SequencerPublisher {
1204
1454
  });
1205
1455
  }
1206
1456
 
1457
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1458
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1459
+
1207
1460
  const simulationResult = await this.l1TxUtils
1208
1461
  .simulate(
1209
1462
  {
@@ -1213,8 +1466,7 @@ export class SequencerPublisher {
1213
1466
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1214
1467
  },
1215
1468
  {
1216
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1217
- time: timestamp + 1n,
1469
+ time: simTs,
1218
1470
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1219
1471
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1220
1472
  },
@@ -1236,7 +1488,19 @@ export class SequencerPublisher {
1236
1488
  logs: [],
1237
1489
  };
1238
1490
  }
1239
- this.log.error(`Failed to simulate propose tx`, viemError);
1491
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1492
+ this.backupFailedTx({
1493
+ id: keccak256(rollupData),
1494
+ failureType: 'simulation',
1495
+ request: { to: this.rollupContract.address, data: rollupData },
1496
+ l1BlockNumber: l1BlockNumber.toString(),
1497
+ error: { message: viemError.message, name: viemError.name },
1498
+ context: {
1499
+ actions: ['propose'],
1500
+ slot: Number(args[0].header.slotNumber),
1501
+ sender: this.getSenderAddress().toString(),
1502
+ },
1503
+ });
1240
1504
  throw err;
1241
1505
  });
1242
1506
 
@@ -1246,16 +1510,15 @@ export class SequencerPublisher {
1246
1510
  private async addProposeTx(
1247
1511
  checkpoint: Checkpoint,
1248
1512
  encodedData: L1ProcessArgs,
1249
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1250
- timestamp: bigint,
1513
+ opts: EnqueueProposeCheckpointOpts = {},
1514
+ preCheck?: () => Promise<void>,
1251
1515
  ): Promise<void> {
1252
1516
  const slot = checkpoint.header.slotNumber;
1253
1517
  const timer = new Timer();
1254
1518
  const kzg = Blob.getViemKzgInstance();
1255
1519
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1256
1520
  encodedData,
1257
- timestamp,
1258
- opts,
1521
+ opts.simulationOverridesPlan,
1259
1522
  );
1260
1523
  const startBlock = await this.l1TxUtils.getBlockNumber();
1261
1524
  const gasLimit = this.l1TxUtils.bumpGasLimit(
@@ -1279,7 +1542,8 @@ export class SequencerPublisher {
1279
1542
  data: rollupData,
1280
1543
  },
1281
1544
  lastValidL2Slot: checkpoint.header.slotNumber,
1282
- gasConfig: { ...opts, gasLimit },
1545
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1546
+ preCheck,
1283
1547
  blobConfig: {
1284
1548
  blobs: encodedData.blobs.map(b => b.data),
1285
1549
  kzg,
@@ -1332,4 +1596,17 @@ export class SequencerPublisher {
1332
1596
  },
1333
1597
  });
1334
1598
  }
1599
+
1600
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1601
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1602
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1603
+ const l1Constants = this.epochCache.getL1Constants();
1604
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1605
+ }
1606
+
1607
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1608
+ private getNextL1SlotTimestamp(): bigint {
1609
+ const l1Constants = this.epochCache.getL1Constants();
1610
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1611
+ }
1335
1612
  }