@aztec/sequencer-client 0.0.1-commit.e558bd1c → 0.0.1-commit.e5a3663dd

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 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +58 -37
  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 +75 -48
  40. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  41. package/dest/publisher/sequencer-publisher.js +434 -141
  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 +705 -223
  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 +23 -8
  57. package/dest/sequencer/metrics.d.ts.map +1 -1
  58. package/dest/sequencer/metrics.js +117 -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 -101
  62. package/dest/sequencer/timetable.d.ts +17 -6
  63. package/dest/sequencer/timetable.d.ts.map +1 -1
  64. package/dest/sequencer/timetable.js +51 -46
  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 +11 -11
  70. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  71. package/dest/test/mock_checkpoint_builder.js +45 -34
  72. package/dest/test/utils.d.ts +3 -3
  73. package/dest/test/utils.d.ts.map +1 -1
  74. package/dest/test/utils.js +10 -8
  75. package/package.json +27 -28
  76. package/src/client/sequencer-client.ts +81 -30
  77. package/src/config.ts +77 -46
  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 +501 -202
  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 +873 -243
  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 +132 -25
  98. package/src/sequencer/sequencer.ts +262 -115
  99. package/src/sequencer/timetable.ts +70 -57
  100. package/src/sequencer/types.ts +1 -1
  101. package/src/test/index.ts +2 -4
  102. package/src/test/mock_checkpoint_builder.ts +63 -49
  103. package/src/test/utils.ts +31 -10
@@ -3,51 +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,
6
+ FeeAssetPriceOracle,
7
7
  type GovernanceProposerContract,
8
8
  type IEmpireBase,
9
9
  MULTI_CALL_3_ADDRESS,
10
10
  Multicall3,
11
11
  RollupContract,
12
- type TallySlashingProposerContract,
12
+ SimulationOverridesBuilder,
13
+ type SimulationOverridesPlan,
14
+ type SlashingProposerContract,
13
15
  type ViemCommitteeAttestations,
14
16
  type ViemHeader,
17
+ buildSimulationOverridesStateOverride,
15
18
  } from '@aztec/ethereum/contracts';
16
19
  import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
17
20
  import {
18
21
  type L1BlobInputs,
19
22
  type L1TxConfig,
20
23
  type L1TxRequest,
24
+ type L1TxUtils,
21
25
  MAX_L1_TX_LIMIT,
22
26
  type TransactionStats,
23
27
  WEI_CONST,
24
28
  } from '@aztec/ethereum/l1-tx-utils';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
29
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
27
30
  import { sumBigint } from '@aztec/foundation/bigint';
28
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
29
31
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
32
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
30
33
  import { pick } from '@aztec/foundation/collection';
31
34
  import type { Fr } from '@aztec/foundation/curves/bn254';
35
+ import { TimeoutError } from '@aztec/foundation/error';
32
36
  import { EthAddress } from '@aztec/foundation/eth-address';
33
37
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
38
  import { type Logger, createLogger } from '@aztec/foundation/log';
39
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
40
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
35
41
  import { bufferToHex } from '@aztec/foundation/string';
36
- import { DateProvider, Timer } from '@aztec/foundation/timer';
42
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
37
43
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
38
44
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
39
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
40
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
41
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
47
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
42
48
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
49
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
50
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
45
51
 
46
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
-
48
- 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';
49
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
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
+
51
80
  /** Arguments to the process method of the rollup contract */
52
81
  type L1ProcessArgs = {
53
82
  /** The L2 block header. */
@@ -60,6 +89,8 @@ type L1ProcessArgs = {
60
89
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
90
  /** Attestations and signers signature */
62
91
  attestationsAndSignersSignature: Signature;
92
+ /** The fee asset price modifier in basis points (from oracle) */
93
+ feeAssetPriceModifier: bigint;
63
94
  };
64
95
 
65
96
  export const Actions = [
@@ -67,16 +98,13 @@ export const Actions = [
67
98
  'invalidate-by-insufficient-attestations',
68
99
  'propose',
69
100
  'governance-signal',
70
- 'empire-slashing-signal',
71
- 'create-empire-payload',
72
- 'execute-empire-payload',
73
101
  'vote-offenses',
74
102
  'execute-slash',
75
103
  ] as const;
76
104
 
77
105
  export type Action = (typeof Actions)[number];
78
106
 
79
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
80
108
 
81
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
82
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -87,6 +115,13 @@ export type InvalidateCheckpointRequest = {
87
115
  gasUsed: bigint;
88
116
  checkpointNumber: CheckpointNumber;
89
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;
90
125
  };
91
126
 
92
127
  interface RequestWithExpiry {
@@ -95,6 +130,8 @@ interface RequestWithExpiry {
95
130
  lastValidL2Slot: SlotNumber;
96
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
97
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
98
135
  checkSuccess: (
99
136
  request: L1TxRequest,
100
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -105,6 +142,7 @@ export class SequencerPublisher {
105
142
  private interrupted = false;
106
143
  private metrics: SequencerPublisherMetrics;
107
144
  public epochCache: EpochCache;
145
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
108
146
 
109
147
  protected governanceLog = createLogger('sequencer:publisher:governance');
110
148
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -112,61 +150,80 @@ export class SequencerPublisher {
112
150
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
151
 
114
152
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
153
+ private payloadProposedCache: Set<string> = new Set<string>();
115
154
 
116
155
  protected log: Logger;
117
156
  protected ethereumSlotDuration: bigint;
157
+ protected aztecSlotDuration: bigint;
158
+
159
+ /** Date provider for wall-clock time. */
160
+ private readonly dateProvider: DateProvider;
118
161
 
119
162
  private blobClient: BlobClientInterface;
120
163
 
121
164
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
122
165
  private proposerAddressForSimulation?: EthAddress;
123
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
+
124
170
  /** L1 fee analyzer for fisherman mode */
125
171
  private l1FeeAnalyzer?: L1FeeAnalyzer;
172
+
173
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
174
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
175
+
176
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
177
+ private readonly interruptibleSleep = new InterruptibleSleep();
178
+
126
179
  // A CALL to a cold address is 2700 gas
127
180
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
181
 
129
182
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
183
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
184
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
185
+ public l1TxUtils: L1TxUtils;
133
186
  public rollupContract: RollupContract;
134
187
  public govProposerContract: GovernanceProposerContract;
135
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
136
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
137
189
 
138
190
  public readonly tracer: Tracer;
139
191
 
140
192
  protected requests: RequestWithExpiry[] = [];
141
193
 
142
194
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
195
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
196
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
144
197
  deps: {
145
198
  telemetry?: TelemetryClient;
146
199
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
200
+ l1TxUtils: L1TxUtils;
148
201
  rollupContract: RollupContract;
149
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
150
203
  governanceProposerContract: GovernanceProposerContract;
151
- slashFactoryContract: SlashFactoryContract;
152
204
  epochCache: EpochCache;
153
205
  dateProvider: DateProvider;
154
206
  metrics: SequencerPublisherMetrics;
155
207
  lastActions: Partial<Record<Action, SlotNumber>>;
156
208
  log?: Logger;
209
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
157
210
  },
158
211
  ) {
159
212
  this.log = deps.log ?? createLogger('sequencer:publisher');
160
213
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
214
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
215
+ this.dateProvider = deps.dateProvider;
161
216
  this.epochCache = deps.epochCache;
162
217
  this.lastActions = deps.lastActions;
163
218
 
164
219
  this.blobClient = deps.blobClient;
220
+ this.dateProvider = deps.dateProvider;
165
221
 
166
222
  const telemetry = deps.telemetry ?? getTelemetryClient();
167
223
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
168
224
  this.tracer = telemetry.getTracer('SequencerPublisher');
169
225
  this.l1TxUtils = deps.l1TxUtils;
226
+ this.getNextPublisher = deps.getNextPublisher;
170
227
 
171
228
  this.rollupContract = deps.rollupContract;
172
229
 
@@ -178,8 +235,6 @@ export class SequencerPublisher {
178
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
179
236
  this.slashingProposerContract = newSlashingProposer;
180
237
  });
181
- this.slashFactoryContract = deps.slashFactoryContract;
182
-
183
238
  // Initialize L1 fee analyzer for fisherman mode
184
239
  if (config.fishermanMode) {
185
240
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -188,12 +243,52 @@ export class SequencerPublisher {
188
243
  createLogger('sequencer:publisher:fee-analyzer'),
189
244
  );
190
245
  }
246
+
247
+ // Initialize fee asset price oracle
248
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
249
+ this.l1TxUtils.client,
250
+ this.rollupContract,
251
+ createLogger('sequencer:publisher:price-oracle'),
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
+ });
191
278
  }
192
279
 
193
280
  public getRollupContract(): RollupContract {
194
281
  return this.rollupContract;
195
282
  }
196
283
 
284
+ /**
285
+ * Gets the fee asset price modifier from the oracle.
286
+ * Returns 0n if the oracle query fails.
287
+ */
288
+ public getFeeAssetPriceModifier(): Promise<bigint> {
289
+ return this.feeAssetPriceOracle.computePriceModifier();
290
+ }
291
+
197
292
  public getSenderAddress() {
198
293
  return this.l1TxUtils.getSenderAddress();
199
294
  }
@@ -218,7 +313,7 @@ export class SequencerPublisher {
218
313
  }
219
314
 
220
315
  public getCurrentL2Slot(): SlotNumber {
221
- return this.epochCache.getEpochAndSlotNow().slot;
316
+ return this.epochCache.getSlotNow();
222
317
  }
223
318
 
224
319
  /**
@@ -296,9 +391,10 @@ export class SequencerPublisher {
296
391
  * - undefined if no valid requests are found OR the tx failed to send.
297
392
  */
298
393
  @trackSpan('SequencerPublisher.sendRequests')
299
- public async sendRequests() {
394
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
300
395
  const requestsToProcess = [...this.requests];
301
396
  this.requests = [];
397
+
302
398
  if (this.interrupted || requestsToProcess.length === 0) {
303
399
  return undefined;
304
400
  }
@@ -331,8 +427,8 @@ export class SequencerPublisher {
331
427
  // @note - we can only have one blob config per bundle
332
428
  // find requests with gas and blob configs
333
429
  // See https://github.com/AztecProtocol/aztec-packages/issues/11513
334
- const gasConfigs = requestsToProcess.filter(request => request.gasConfig).map(request => request.gasConfig);
335
- 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);
336
432
 
337
433
  if (blobConfigs.length > 1) {
338
434
  throw new Error('Multiple blob configs found');
@@ -361,19 +457,36 @@ export class SequencerPublisher {
361
457
  validRequests.sort((a, b) => compareActions(a.action, b.action));
362
458
 
363
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
+
364
477
  this.log.debug('Forwarding transactions', {
365
478
  validRequests: validRequests.map(request => request.action),
366
479
  txConfig,
367
480
  });
368
- const result = await Multicall3.forward(
369
- validRequests.map(request => request.request),
370
- this.l1TxUtils,
371
- txConfig,
372
- blobConfig,
373
- this.rollupContract.address,
374
- 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,
375
489
  );
376
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
377
490
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
378
491
  } catch (err) {
379
492
  const viemError = formatViemError(err);
@@ -391,16 +504,127 @@ export class SequencerPublisher {
391
504
  }
392
505
  }
393
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
+
394
595
  private callbackBundledTransactions(
395
596
  requests: RequestWithExpiry[],
396
- result?: { receipt: TransactionReceipt } | FormattedViemError,
597
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
598
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
397
599
  ) {
398
600
  const actionsListStr = requests.map(r => r.action).join(', ');
399
601
  if (result instanceof FormattedViemError) {
400
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
+ });
401
616
  return { failedActions: requests.map(r => r.action) };
402
617
  } else {
403
- 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
+ });
404
628
  const successfulActions: Action[] = [];
405
629
  const failedActions: Action[] = [];
406
630
  for (const request of requests) {
@@ -410,27 +634,54 @@ export class SequencerPublisher {
410
634
  failedActions.push(request.action);
411
635
  }
412
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
+ }
413
661
  return { successfulActions, failedActions };
414
662
  }
415
663
  }
416
664
 
417
665
  /**
418
- * @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
419
667
  * @param tipArchive - The archive to check
420
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
421
669
  */
422
- public canProposeAtNextEthBlock(
423
- tipArchive: Fr,
424
- msgSender: EthAddress,
425
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
426
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
427
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
428
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
429
673
 
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
675
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
676
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
677
+
430
678
  return this.rollupContract
431
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
432
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
433
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
434
685
  .catch(err => {
435
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
436
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -442,6 +693,7 @@ export class SequencerPublisher {
442
693
  return undefined;
443
694
  });
444
695
  }
696
+
445
697
  /**
446
698
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
447
699
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -451,13 +703,13 @@ export class SequencerPublisher {
451
703
  @trackSpan('SequencerPublisher.validateBlockHeader')
452
704
  public async validateBlockHeader(
453
705
  header: CheckpointHeader,
454
- opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
455
707
  ): Promise<void> {
456
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
457
709
 
458
710
  const args = [
459
711
  header.toViem(),
460
- CommitteeAttestationsAndSigners.empty().getPackedAttestations(),
712
+ CommitteeAttestationsAndSigners.packAttestations([]),
461
713
  [], // no signers
462
714
  Signature.empty().toViemSignature(),
463
715
  `0x${'0'.repeat(64)}`, // 32 empty bytes
@@ -465,10 +717,8 @@ export class SequencerPublisher {
465
717
  flags,
466
718
  ] as const;
467
719
 
468
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
469
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
470
- opts?.forcePendingCheckpointNumber,
471
- );
720
+ const ts = this.getSimulationTimestamp(header.slotNumber);
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
472
722
  let balance = 0n;
473
723
  if (this.config.fishermanMode) {
474
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -488,7 +738,7 @@ export class SequencerPublisher {
488
738
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
489
739
  from: MULTI_CALL_3_ADDRESS,
490
740
  },
491
- { time: ts + 1n },
741
+ { time: ts },
492
742
  stateOverrides,
493
743
  );
494
744
  this.log.debug(`Simulated validateHeader`);
@@ -521,6 +771,8 @@ export class SequencerPublisher {
521
771
  const request = this.buildInvalidateCheckpointRequest(validationResult);
522
772
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
523
773
 
774
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
775
+
524
776
  try {
525
777
  const { gasUsed } = await this.l1TxUtils.simulate(
526
778
  request,
@@ -539,6 +791,7 @@ export class SequencerPublisher {
539
791
  gasUsed,
540
792
  checkpointNumber,
541
793
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
794
+ lastArchive: validationResult.checkpoint.lastArchive,
542
795
  reason,
543
796
  };
544
797
  } catch (err) {
@@ -551,8 +804,8 @@ export class SequencerPublisher {
551
804
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
552
805
  { ...logData, request, error: viemError.message },
553
806
  );
554
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
555
- if (latestPendingCheckpointNumber < checkpointNumber) {
807
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
808
+ if (latestProposedCheckpointNumber < checkpointNumber) {
556
809
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
557
810
  return undefined;
558
811
  } else {
@@ -572,6 +825,18 @@ export class SequencerPublisher {
572
825
 
573
826
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
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
+ });
575
840
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
576
841
  }
577
842
  }
@@ -585,9 +850,7 @@ export class SequencerPublisher {
585
850
  const logData = { ...checkpoint, reason };
586
851
  this.log.debug(`Building invalidate checkpoint ${checkpoint.checkpointNumber} request`, logData);
587
852
 
588
- const attestationsAndSigners = new CommitteeAttestationsAndSigners(
589
- validationResult.attestations,
590
- ).getPackedAttestations();
853
+ const attestationsAndSigners = CommitteeAttestationsAndSigners.packAttestations(validationResult.attestations);
591
854
 
592
855
  if (reason === 'invalid-attestation') {
593
856
  return this.rollupContract.buildInvalidateBadAttestationRequest(
@@ -614,27 +877,10 @@ export class SequencerPublisher {
614
877
  checkpoint: Checkpoint,
615
878
  attestationsAndSigners: CommitteeAttestationsAndSigners,
616
879
  attestationsAndSignersSignature: Signature,
617
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
618
- ): Promise<bigint> {
619
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
620
-
621
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
622
- // If we have no attestations, we still need to provide the empty attestations
623
- // so that the committee is recalculated correctly
624
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
625
- // if (ignoreSignatures) {
626
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
627
- // if (!committee) {
628
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
629
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
630
- // }
631
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
632
- // CommitteeAttestation.fromAddress(committeeMember),
633
- // );
634
- // }
635
-
880
+ simulationOverridesPlan?: SimulationOverridesPlan,
881
+ ): Promise<void> {
636
882
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
883
+ const blobs = await getBlobsPerL1Block(blobFields);
638
884
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
885
 
640
886
  const args = [
@@ -642,7 +888,7 @@ export class SequencerPublisher {
642
888
  header: checkpoint.header.toViem(),
643
889
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
890
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
891
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
892
  },
647
893
  },
648
894
  attestationsAndSigners.getPackedAttestations(),
@@ -651,13 +897,11 @@ export class SequencerPublisher {
651
897
  blobInput,
652
898
  ] as const;
653
899
 
654
- await this.simulateProposeTx(args, ts, options);
655
- return ts;
900
+ await this.simulateProposeTx(args, simulationOverridesPlan);
656
901
  }
657
902
 
658
903
  private async enqueueCastSignalHelper(
659
904
  slotNumber: SlotNumber,
660
- timestamp: bigint,
661
905
  signalType: GovernanceSignalAction,
662
906
  payload: EthAddress,
663
907
  base: IEmpireBase,
@@ -691,6 +935,32 @@ export class SequencerPublisher {
691
935
  return false;
692
936
  }
693
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
+
694
964
  const cachedLastVote = this.lastActions[signalType];
695
965
  this.lastActions[signalType] = slotNumber;
696
966
  const action = signalType;
@@ -709,11 +979,30 @@ export class SequencerPublisher {
709
979
  lastValidL2Slot: slotNumber,
710
980
  });
711
981
 
982
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
983
+ const timestamp = this.getSimulationTimestamp(slotNumber);
984
+
712
985
  try {
713
986
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
987
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
988
  } catch (err) {
716
- 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
+ });
717
1006
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
1007
  }
719
1008
 
@@ -764,19 +1053,16 @@ export class SequencerPublisher {
764
1053
  /**
765
1054
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
766
1055
  * @param slotNumber - The slot number to cast a signal for.
767
- * @param timestamp - The timestamp of the slot to cast a signal for.
768
1056
  * @returns True if the signal was successfully enqueued, false otherwise.
769
1057
  */
770
1058
  public enqueueGovernanceCastSignal(
771
1059
  governancePayload: EthAddress,
772
1060
  slotNumber: SlotNumber,
773
- timestamp: bigint,
774
1061
  signerAddress: EthAddress,
775
1062
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
776
1063
  ): Promise<boolean> {
777
1064
  return this.enqueueCastSignalHelper(
778
1065
  slotNumber,
779
- timestamp,
780
1066
  'governance-signal',
781
1067
  governancePayload,
782
1068
  this.govProposerContract,
@@ -789,7 +1075,6 @@ export class SequencerPublisher {
789
1075
  public async enqueueSlashingActions(
790
1076
  actions: ProposerSlashAction[],
791
1077
  slotNumber: SlotNumber,
792
- timestamp: bigint,
793
1078
  signerAddress: EthAddress,
794
1079
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
795
1080
  ): Promise<boolean> {
@@ -800,58 +1085,6 @@ export class SequencerPublisher {
800
1085
 
801
1086
  for (const action of actions) {
802
1087
  switch (action.type) {
803
- case 'vote-empire-payload': {
804
- if (this.slashingProposerContract?.type !== 'empire') {
805
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
806
- break;
807
- }
808
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
809
- signerAddress,
810
- });
811
- await this.enqueueCastSignalHelper(
812
- slotNumber,
813
- timestamp,
814
- 'empire-slashing-signal',
815
- action.payload,
816
- this.slashingProposerContract,
817
- signerAddress,
818
- signer,
819
- );
820
- break;
821
- }
822
-
823
- case 'create-empire-payload': {
824
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
825
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
826
- await this.simulateAndEnqueueRequest(
827
- 'create-empire-payload',
828
- request,
829
- (receipt: TransactionReceipt) =>
830
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
831
- slotNumber,
832
- timestamp,
833
- );
834
- break;
835
- }
836
-
837
- case 'execute-empire-payload': {
838
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
839
- if (this.slashingProposerContract?.type !== 'empire') {
840
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
841
- return false;
842
- }
843
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
844
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
845
- await this.simulateAndEnqueueRequest(
846
- 'execute-empire-payload',
847
- request,
848
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
849
- slotNumber,
850
- timestamp,
851
- );
852
- break;
853
- }
854
-
855
1088
  case 'vote-offenses': {
856
1089
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
857
1090
  slotNumber,
@@ -859,19 +1092,17 @@ export class SequencerPublisher {
859
1092
  votesCount: action.votes.length,
860
1093
  signerAddress,
861
1094
  });
862
- if (this.slashingProposerContract?.type !== 'tally') {
863
- 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');
864
1097
  return false;
865
1098
  }
866
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
867
1099
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
868
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1100
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
869
1101
  await this.simulateAndEnqueueRequest(
870
1102
  'vote-offenses',
871
1103
  request,
872
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1104
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
873
1105
  slotNumber,
874
- timestamp,
875
1106
  );
876
1107
  break;
877
1108
  }
@@ -882,18 +1113,20 @@ export class SequencerPublisher {
882
1113
  round: action.round,
883
1114
  signerAddress,
884
1115
  });
885
- if (this.slashingProposerContract?.type !== 'tally') {
886
- 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');
887
1118
  return false;
888
1119
  }
889
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
890
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1120
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1121
+ action.round,
1122
+ action.committees,
1123
+ );
891
1124
  await this.simulateAndEnqueueRequest(
892
1125
  'execute-slash',
893
- request,
894
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1126
+ executeRequest,
1127
+ (receipt: TransactionReceipt) =>
1128
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
895
1129
  slotNumber,
896
- timestamp,
897
1130
  );
898
1131
  break;
899
1132
  }
@@ -913,22 +1146,25 @@ export class SequencerPublisher {
913
1146
  checkpoint: Checkpoint,
914
1147
  attestationsAndSigners: CommitteeAttestationsAndSigners,
915
1148
  attestationsAndSignersSignature: Signature,
916
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1149
+ opts: EnqueueProposeCheckpointOpts = {},
917
1150
  ): Promise<void> {
918
1151
  const checkpointHeader = checkpoint.header;
919
1152
 
920
1153
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
1154
+ const blobs = await getBlobsPerL1Block(blobFields);
922
1155
 
923
- const proposeTxArgs = {
1156
+ const proposeTxArgs: L1ProcessArgs = {
924
1157
  header: checkpointHeader,
925
1158
  archive: checkpoint.archive.root.toBuffer(),
926
1159
  blobs,
927
1160
  attestationsAndSigners,
928
1161
  attestationsAndSignersSignature,
1162
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
1163
  };
930
1164
 
931
- let ts: bigint;
1165
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1166
+ .withoutBlobCheck()
1167
+ .build();
932
1168
 
933
1169
  try {
934
1170
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
@@ -936,23 +1172,47 @@ export class SequencerPublisher {
936
1172
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
937
1173
  // make time consistency checks break.
938
1174
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
939
- ts = await this.validateCheckpointForSubmission(
1175
+ await this.validateCheckpointForSubmission(
940
1176
  checkpoint,
941
1177
  attestationsAndSigners,
942
1178
  attestationsAndSignersSignature,
943
- opts,
1179
+ simulationOverridesPlan,
944
1180
  );
945
1181
  } catch (err: any) {
946
1182
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
947
1183
  ...checkpoint.getStats(),
948
1184
  slotNumber: checkpoint.header.slotNumber,
949
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1185
+ simulationOverridesPlan,
950
1186
  });
951
1187
  throw err;
952
1188
  }
953
1189
 
954
- this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
955
- 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
+ );
956
1216
  }
957
1217
 
958
1218
  public enqueueInvalidateCheckpoint(
@@ -995,8 +1255,8 @@ export class SequencerPublisher {
995
1255
  request: L1TxRequest,
996
1256
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
997
1257
  slotNumber: SlotNumber,
998
- timestamp: bigint,
999
1258
  ) {
1259
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1000
1260
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1001
1261
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1002
1262
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1008,15 +1268,31 @@ export class SequencerPublisher {
1008
1268
 
1009
1269
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1010
1270
 
1271
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1272
+
1011
1273
  let gasUsed: bigint;
1012
1274
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1275
+
1013
1276
  try {
1014
- ({ 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));
1015
1278
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1016
1279
  } catch (err) {
1017
1280
  const viemError = formatViemError(err, simulateAbi);
1018
1281
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
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
+
1020
1296
  return false;
1021
1297
  }
1022
1298
 
@@ -1056,6 +1332,7 @@ export class SequencerPublisher {
1056
1332
  */
1057
1333
  public interrupt() {
1058
1334
  this.interrupted = true;
1335
+ this.interruptibleSleep.interrupt();
1059
1336
  this.l1TxUtils.interrupt();
1060
1337
  }
1061
1338
 
@@ -1065,11 +1342,7 @@ export class SequencerPublisher {
1065
1342
  this.l1TxUtils.restart();
1066
1343
  }
1067
1344
 
1068
- private async prepareProposeTx(
1069
- encodedData: L1ProcessArgs,
1070
- timestamp: bigint,
1071
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1072
- ) {
1345
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
1073
1346
  const kzg = Blob.getViemKzgInstance();
1074
1347
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1075
1348
  this.log.debug('Validating blob input', { blobInput });
@@ -1100,9 +1373,27 @@ export class SequencerPublisher {
1100
1373
  kzg,
1101
1374
  },
1102
1375
  )
1103
- .catch(err => {
1104
- const { message, metaMessages } = formatViemError(err);
1105
- 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
+ });
1106
1397
  throw new Error('Failed to validate blobs');
1107
1398
  });
1108
1399
  }
@@ -1113,8 +1404,7 @@ export class SequencerPublisher {
1113
1404
  header: encodedData.header.toViem(),
1114
1405
  archive: toHex(encodedData.archive),
1115
1406
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1407
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1408
  },
1119
1409
  },
1120
1410
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1123,7 +1413,7 @@ export class SequencerPublisher {
1123
1413
  blobInput,
1124
1414
  ] as const;
1125
1415
 
1126
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1416
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1127
1417
 
1128
1418
  return { args, blobEvaluationGas, rollupData, simulationResult };
1129
1419
  }
@@ -1131,7 +1421,6 @@ export class SequencerPublisher {
1131
1421
  /**
1132
1422
  * Simulates the propose tx with eth_simulateV1
1133
1423
  * @param args - The propose tx args
1134
- * @param timestamp - The timestamp to simulate proposal at
1135
1424
  * @returns The simulation result
1136
1425
  */
1137
1426
  private async simulateProposeTx(
@@ -1140,7 +1429,7 @@ export class SequencerPublisher {
1140
1429
  readonly header: ViemHeader;
1141
1430
  readonly archive: `0x${string}`;
1142
1431
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1432
+ readonly feeAssetPriceModifier: bigint;
1144
1433
  };
1145
1434
  },
1146
1435
  ViemCommitteeAttestations,
@@ -1148,8 +1437,7 @@ export class SequencerPublisher {
1148
1437
  ViemSignature,
1149
1438
  `0x${string}`,
1150
1439
  ],
1151
- timestamp: bigint,
1152
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1440
+ simulationOverridesPlan?: SimulationOverridesPlan,
1153
1441
  ) {
1154
1442
  const rollupData = encodeFunctionData({
1155
1443
  abi: RollupAbi,
@@ -1157,23 +1445,7 @@ export class SequencerPublisher {
1157
1445
  args,
1158
1446
  });
1159
1447
 
1160
- // override the pending checkpoint number if requested
1161
- const forcePendingCheckpointNumberStateDiff = (
1162
- options.forcePendingCheckpointNumber !== undefined
1163
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1164
- : []
1165
- ).flatMap(override => override.stateDiff ?? []);
1166
-
1167
- const stateOverrides: StateOverride = [
1168
- {
1169
- address: this.rollupContract.address,
1170
- // @note we override checkBlob to false since blobs are not part simulate()
1171
- stateDiff: [
1172
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1173
- ...forcePendingCheckpointNumberStateDiff,
1174
- ],
1175
- },
1176
- ];
1448
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1177
1449
  // In fisherman mode, simulate as the proposer but with sufficient balance
1178
1450
  if (this.proposerAddressForSimulation) {
1179
1451
  stateOverrides.push({
@@ -1182,6 +1454,9 @@ export class SequencerPublisher {
1182
1454
  });
1183
1455
  }
1184
1456
 
1457
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1458
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1459
+
1185
1460
  const simulationResult = await this.l1TxUtils
1186
1461
  .simulate(
1187
1462
  {
@@ -1191,8 +1466,7 @@ export class SequencerPublisher {
1191
1466
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1192
1467
  },
1193
1468
  {
1194
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1195
- time: timestamp + 1n,
1469
+ time: simTs,
1196
1470
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1197
1471
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1198
1472
  },
@@ -1214,7 +1488,19 @@ export class SequencerPublisher {
1214
1488
  logs: [],
1215
1489
  };
1216
1490
  }
1217
- 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
+ });
1218
1504
  throw err;
1219
1505
  });
1220
1506
 
@@ -1224,16 +1510,15 @@ export class SequencerPublisher {
1224
1510
  private async addProposeTx(
1225
1511
  checkpoint: Checkpoint,
1226
1512
  encodedData: L1ProcessArgs,
1227
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1228
- timestamp: bigint,
1513
+ opts: EnqueueProposeCheckpointOpts = {},
1514
+ preCheck?: () => Promise<void>,
1229
1515
  ): Promise<void> {
1230
1516
  const slot = checkpoint.header.slotNumber;
1231
1517
  const timer = new Timer();
1232
1518
  const kzg = Blob.getViemKzgInstance();
1233
1519
  const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1234
1520
  encodedData,
1235
- timestamp,
1236
- opts,
1521
+ opts.simulationOverridesPlan,
1237
1522
  );
1238
1523
  const startBlock = await this.l1TxUtils.getBlockNumber();
1239
1524
  const gasLimit = this.l1TxUtils.bumpGasLimit(
@@ -1257,7 +1542,8 @@ export class SequencerPublisher {
1257
1542
  data: rollupData,
1258
1543
  },
1259
1544
  lastValidL2Slot: checkpoint.header.slotNumber,
1260
- gasConfig: { ...opts, gasLimit },
1545
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1546
+ preCheck,
1261
1547
  blobConfig: {
1262
1548
  blobs: encodedData.blobs.map(b => b.data),
1263
1549
  kzg,
@@ -1310,4 +1596,17 @@ export class SequencerPublisher {
1310
1596
  },
1311
1597
  });
1312
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
+ }
1313
1612
  }