@aztec/sequencer-client 0.0.1-commit.e2b2873ed → 0.0.1-commit.e304674f1

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 (85) hide show
  1. package/dest/client/sequencer-client.d.ts +15 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +60 -30
  4. package/dest/config.d.ts +26 -6
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +44 -21
  7. package/dest/global_variable_builder/global_builder.d.ts +15 -11
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +29 -25
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +47 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +121 -42
  15. package/dest/publisher/index.d.ts +2 -1
  16. package/dest/publisher/index.d.ts.map +1 -1
  17. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  26. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  29. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  32. package/dest/publisher/sequencer-publisher.d.ts +82 -37
  33. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher.js +430 -118
  35. package/dest/sequencer/checkpoint_proposal_job.d.ts +36 -9
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  37. package/dest/sequencer/checkpoint_proposal_job.js +361 -192
  38. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  39. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  40. package/dest/sequencer/checkpoint_voter.js +2 -5
  41. package/dest/sequencer/events.d.ts +2 -1
  42. package/dest/sequencer/events.d.ts.map +1 -1
  43. package/dest/sequencer/metrics.d.ts +21 -5
  44. package/dest/sequencer/metrics.d.ts.map +1 -1
  45. package/dest/sequencer/metrics.js +97 -15
  46. package/dest/sequencer/sequencer.d.ts +40 -17
  47. package/dest/sequencer/sequencer.d.ts.map +1 -1
  48. package/dest/sequencer/sequencer.js +152 -95
  49. package/dest/sequencer/timetable.d.ts +7 -3
  50. package/dest/sequencer/timetable.d.ts.map +1 -1
  51. package/dest/sequencer/timetable.js +21 -12
  52. package/dest/sequencer/types.d.ts +2 -2
  53. package/dest/sequencer/types.d.ts.map +1 -1
  54. package/dest/test/index.d.ts +3 -5
  55. package/dest/test/index.d.ts.map +1 -1
  56. package/dest/test/mock_checkpoint_builder.d.ts +11 -11
  57. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  58. package/dest/test/mock_checkpoint_builder.js +45 -34
  59. package/dest/test/utils.d.ts +3 -3
  60. package/dest/test/utils.d.ts.map +1 -1
  61. package/dest/test/utils.js +5 -4
  62. package/package.json +27 -28
  63. package/src/client/sequencer-client.ts +76 -30
  64. package/src/config.ts +56 -27
  65. package/src/global_variable_builder/global_builder.ts +38 -27
  66. package/src/global_variable_builder/index.ts +1 -1
  67. package/src/publisher/config.ts +153 -43
  68. package/src/publisher/index.ts +3 -0
  69. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  70. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  71. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  72. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  73. package/src/publisher/sequencer-publisher-factory.ts +38 -9
  74. package/src/publisher/sequencer-publisher.ts +503 -168
  75. package/src/sequencer/README.md +81 -12
  76. package/src/sequencer/checkpoint_proposal_job.ts +471 -201
  77. package/src/sequencer/checkpoint_voter.ts +1 -12
  78. package/src/sequencer/events.ts +1 -1
  79. package/src/sequencer/metrics.ts +106 -18
  80. package/src/sequencer/sequencer.ts +216 -109
  81. package/src/sequencer/timetable.ts +26 -15
  82. package/src/sequencer/types.ts +1 -1
  83. package/src/test/index.ts +2 -4
  84. package/src/test/mock_checkpoint_builder.ts +63 -49
  85. package/src/test/utils.ts +5 -2
@@ -3,13 +3,14 @@ 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
+ type FeeHeader,
7
8
  type GovernanceProposerContract,
8
9
  type IEmpireBase,
9
10
  MULTI_CALL_3_ADDRESS,
10
11
  Multicall3,
11
12
  RollupContract,
12
- type TallySlashingProposerContract,
13
+ type SlashingProposerContract,
13
14
  type ViemCommitteeAttestations,
14
15
  type ViemHeader,
15
16
  } from '@aztec/ethereum/contracts';
@@ -18,36 +19,64 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
21
23
  MAX_L1_TX_LIMIT,
22
24
  type TransactionStats,
23
25
  WEI_CONST,
24
26
  } from '@aztec/ethereum/l1-tx-utils';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
27
28
  import { sumBigint } from '@aztec/foundation/bigint';
28
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
29
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
+ import { trimmedBytesLength } from '@aztec/foundation/buffer';
30
32
  import { pick } from '@aztec/foundation/collection';
31
33
  import type { Fr } from '@aztec/foundation/curves/bn254';
34
+ import { TimeoutError } from '@aztec/foundation/error';
32
35
  import { EthAddress } from '@aztec/foundation/eth-address';
33
36
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
37
  import { type Logger, createLogger } from '@aztec/foundation/log';
38
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
39
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
35
40
  import { bufferToHex } from '@aztec/foundation/string';
36
- import { DateProvider, Timer } from '@aztec/foundation/timer';
41
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
37
42
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
38
43
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
39
44
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
40
45
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
41
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
46
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
42
47
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
48
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
49
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
45
50
 
46
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
-
48
- import type { PublisherConfig, TxSenderConfig } from './config.js';
51
+ import {
52
+ type Hex,
53
+ type StateOverride,
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,8 @@ 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;
90
120
  };
91
121
 
92
122
  interface RequestWithExpiry {
@@ -95,6 +125,8 @@ interface RequestWithExpiry {
95
125
  lastValidL2Slot: SlotNumber;
96
126
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
97
127
  blobConfig?: L1BlobInputs;
128
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
129
+ preCheck?: () => Promise<void>;
98
130
  checkSuccess: (
99
131
  request: L1TxRequest,
100
132
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -105,6 +137,7 @@ export class SequencerPublisher {
105
137
  private interrupted = false;
106
138
  private metrics: SequencerPublisherMetrics;
107
139
  public epochCache: EpochCache;
140
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
108
141
 
109
142
  protected governanceLog = createLogger('sequencer:publisher:governance');
110
143
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -112,61 +145,80 @@ export class SequencerPublisher {
112
145
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
146
 
114
147
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
148
+ private payloadProposedCache: Set<string> = new Set<string>();
115
149
 
116
150
  protected log: Logger;
117
151
  protected ethereumSlotDuration: bigint;
152
+ protected aztecSlotDuration: bigint;
153
+
154
+ /** Date provider for wall-clock time. */
155
+ private readonly dateProvider: DateProvider;
118
156
 
119
157
  private blobClient: BlobClientInterface;
120
158
 
121
159
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
122
160
  private proposerAddressForSimulation?: EthAddress;
123
161
 
162
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
163
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
164
+
124
165
  /** L1 fee analyzer for fisherman mode */
125
166
  private l1FeeAnalyzer?: L1FeeAnalyzer;
167
+
168
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
169
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
170
+
171
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
172
+ private readonly interruptibleSleep = new InterruptibleSleep();
173
+
126
174
  // A CALL to a cold address is 2700 gas
127
175
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
176
 
129
177
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
178
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
179
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
180
+ public l1TxUtils: L1TxUtils;
133
181
  public rollupContract: RollupContract;
134
182
  public govProposerContract: GovernanceProposerContract;
135
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
136
- public slashFactoryContract: SlashFactoryContract;
183
+ public slashingProposerContract: SlashingProposerContract | undefined;
137
184
 
138
185
  public readonly tracer: Tracer;
139
186
 
140
187
  protected requests: RequestWithExpiry[] = [];
141
188
 
142
189
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
190
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
191
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
144
192
  deps: {
145
193
  telemetry?: TelemetryClient;
146
194
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
195
+ l1TxUtils: L1TxUtils;
148
196
  rollupContract: RollupContract;
149
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
197
+ slashingProposerContract: SlashingProposerContract | undefined;
150
198
  governanceProposerContract: GovernanceProposerContract;
151
- slashFactoryContract: SlashFactoryContract;
152
199
  epochCache: EpochCache;
153
200
  dateProvider: DateProvider;
154
201
  metrics: SequencerPublisherMetrics;
155
202
  lastActions: Partial<Record<Action, SlotNumber>>;
156
203
  log?: Logger;
204
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
157
205
  },
158
206
  ) {
159
207
  this.log = deps.log ?? createLogger('sequencer:publisher');
160
208
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
209
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
210
+ this.dateProvider = deps.dateProvider;
161
211
  this.epochCache = deps.epochCache;
162
212
  this.lastActions = deps.lastActions;
163
213
 
164
214
  this.blobClient = deps.blobClient;
215
+ this.dateProvider = deps.dateProvider;
165
216
 
166
217
  const telemetry = deps.telemetry ?? getTelemetryClient();
167
218
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
168
219
  this.tracer = telemetry.getTracer('SequencerPublisher');
169
220
  this.l1TxUtils = deps.l1TxUtils;
221
+ this.getNextPublisher = deps.getNextPublisher;
170
222
 
171
223
  this.rollupContract = deps.rollupContract;
172
224
 
@@ -178,8 +230,6 @@ export class SequencerPublisher {
178
230
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
179
231
  this.slashingProposerContract = newSlashingProposer;
180
232
  });
181
- this.slashFactoryContract = deps.slashFactoryContract;
182
-
183
233
  // Initialize L1 fee analyzer for fisherman mode
184
234
  if (config.fishermanMode) {
185
235
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -188,12 +238,52 @@ export class SequencerPublisher {
188
238
  createLogger('sequencer:publisher:fee-analyzer'),
189
239
  );
190
240
  }
241
+
242
+ // Initialize fee asset price oracle
243
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
244
+ this.l1TxUtils.client,
245
+ this.rollupContract,
246
+ createLogger('sequencer:publisher:price-oracle'),
247
+ );
248
+
249
+ // Initialize failed L1 tx store (optional, for test networks)
250
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
251
+ }
252
+
253
+ /**
254
+ * Backs up a failed L1 transaction to the configured store for debugging.
255
+ * Does nothing if no store is configured.
256
+ */
257
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
258
+ if (!this.failedTxStore) {
259
+ return;
260
+ }
261
+
262
+ const tx: FailedL1Tx = {
263
+ ...failedTx,
264
+ timestamp: Date.now(),
265
+ };
266
+
267
+ // Fire and forget - don't block on backup
268
+ void this.failedTxStore
269
+ .then(store => store?.saveFailedTx(tx))
270
+ .catch(err => {
271
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
272
+ });
191
273
  }
192
274
 
193
275
  public getRollupContract(): RollupContract {
194
276
  return this.rollupContract;
195
277
  }
196
278
 
279
+ /**
280
+ * Gets the fee asset price modifier from the oracle.
281
+ * Returns 0n if the oracle query fails.
282
+ */
283
+ public getFeeAssetPriceModifier(): Promise<bigint> {
284
+ return this.feeAssetPriceOracle.computePriceModifier();
285
+ }
286
+
197
287
  public getSenderAddress() {
198
288
  return this.l1TxUtils.getSenderAddress();
199
289
  }
@@ -218,7 +308,7 @@ export class SequencerPublisher {
218
308
  }
219
309
 
220
310
  public getCurrentL2Slot(): SlotNumber {
221
- return this.epochCache.getEpochAndSlotNow().slot;
311
+ return this.epochCache.getSlotNow();
222
312
  }
223
313
 
224
314
  /**
@@ -296,9 +386,10 @@ export class SequencerPublisher {
296
386
  * - undefined if no valid requests are found OR the tx failed to send.
297
387
  */
298
388
  @trackSpan('SequencerPublisher.sendRequests')
299
- public async sendRequests() {
389
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
300
390
  const requestsToProcess = [...this.requests];
301
391
  this.requests = [];
392
+
302
393
  if (this.interrupted || requestsToProcess.length === 0) {
303
394
  return undefined;
304
395
  }
@@ -331,8 +422,8 @@ export class SequencerPublisher {
331
422
  // @note - we can only have one blob config per bundle
332
423
  // find requests with gas and blob configs
333
424
  // 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);
425
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
426
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
336
427
 
337
428
  if (blobConfigs.length > 1) {
338
429
  throw new Error('Multiple blob configs found');
@@ -361,19 +452,36 @@ export class SequencerPublisher {
361
452
  validRequests.sort((a, b) => compareActions(a.action, b.action));
362
453
 
363
454
  try {
455
+ // Capture context for failed tx backup before sending
456
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
457
+ const multicallData = encodeFunctionData({
458
+ abi: multicall3Abi,
459
+ functionName: 'aggregate3',
460
+ args: [
461
+ validRequests.map(r => ({
462
+ target: r.request.to!,
463
+ callData: r.request.data!,
464
+ allowFailure: true,
465
+ })),
466
+ ],
467
+ });
468
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
469
+
470
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
471
+
364
472
  this.log.debug('Forwarding transactions', {
365
473
  validRequests: validRequests.map(request => request.action),
366
474
  txConfig,
367
475
  });
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,
476
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
477
+ if (result === undefined) {
478
+ return undefined;
479
+ }
480
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
481
+ validRequests,
482
+ result,
483
+ txContext,
375
484
  );
376
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
377
485
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
378
486
  } catch (err) {
379
487
  const viemError = formatViemError(err);
@@ -391,16 +499,127 @@ export class SequencerPublisher {
391
499
  }
392
500
  }
393
501
 
502
+ /**
503
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
504
+ * failure occurs (i.e. the tx never reached the chain).
505
+ * On-chain reverts and simulation errors are returned as-is without rotation.
506
+ */
507
+ private async forwardWithPublisherRotation(
508
+ validRequests: RequestWithExpiry[],
509
+ txConfig: RequestWithExpiry['gasConfig'],
510
+ blobConfig: L1BlobInputs | undefined,
511
+ ) {
512
+ const triedAddresses: EthAddress[] = [];
513
+ let currentPublisher = this.l1TxUtils;
514
+
515
+ while (true) {
516
+ triedAddresses.push(currentPublisher.getSenderAddress());
517
+ try {
518
+ const result = await Multicall3.forward(
519
+ validRequests.map(r => r.request),
520
+ currentPublisher,
521
+ txConfig,
522
+ blobConfig,
523
+ this.rollupContract.address,
524
+ this.log,
525
+ );
526
+ this.l1TxUtils = currentPublisher;
527
+ return result;
528
+ } catch (err) {
529
+ if (err instanceof TimeoutError) {
530
+ throw err;
531
+ }
532
+ const viemError = formatViemError(err);
533
+ if (!this.getNextPublisher) {
534
+ this.log.error('Failed to publish bundled transactions', viemError);
535
+ return undefined;
536
+ }
537
+ this.log.warn(
538
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
539
+ viemError,
540
+ );
541
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
542
+ if (!nextPublisher) {
543
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
544
+ return undefined;
545
+ }
546
+ currentPublisher = nextPublisher;
547
+ }
548
+ }
549
+ }
550
+
551
+ /*
552
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
553
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
554
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
555
+ */
556
+ public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
557
+ const ms = submitAfter.getTime() - this.dateProvider.now();
558
+ if (ms > 0) {
559
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
560
+ await this.interruptibleSleep.sleep(ms);
561
+ }
562
+ if (this.interrupted) {
563
+ return undefined;
564
+ }
565
+
566
+ // Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
567
+ const validRequests: RequestWithExpiry[] = [];
568
+ for (const request of this.requests) {
569
+ if (!request.preCheck) {
570
+ validRequests.push(request);
571
+ continue;
572
+ }
573
+
574
+ try {
575
+ await request.preCheck();
576
+ validRequests.push(request);
577
+ } catch (err) {
578
+ this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
579
+ }
580
+ }
581
+
582
+ this.requests = validRequests;
583
+ if (this.requests.length === 0) {
584
+ return undefined;
585
+ }
586
+
587
+ return this.sendRequests();
588
+ }
589
+
394
590
  private callbackBundledTransactions(
395
591
  requests: RequestWithExpiry[],
396
- result?: { receipt: TransactionReceipt } | FormattedViemError,
592
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
593
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
397
594
  ) {
398
595
  const actionsListStr = requests.map(r => r.action).join(', ');
399
596
  if (result instanceof FormattedViemError) {
400
597
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
598
+ this.backupFailedTx({
599
+ id: keccak256(txContext.multicallData),
600
+ failureType: 'send-error',
601
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
602
+ blobData: txContext.blobData,
603
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
604
+ error: { message: result.message, name: result.name },
605
+ context: {
606
+ actions: requests.map(r => r.action),
607
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
608
+ sender: this.getSenderAddress().toString(),
609
+ },
610
+ });
401
611
  return { failedActions: requests.map(r => r.action) };
402
612
  } else {
403
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
613
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
614
+ result,
615
+ requests: requests.map(r => ({
616
+ ...r,
617
+ // Avoid logging large blob data
618
+ blobConfig: r.blobConfig
619
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
620
+ : undefined,
621
+ })),
622
+ });
404
623
  const successfulActions: Action[] = [];
405
624
  const failedActions: Action[] = [];
406
625
  for (const request of requests) {
@@ -410,26 +629,59 @@ export class SequencerPublisher {
410
629
  failedActions.push(request.action);
411
630
  }
412
631
  }
632
+ // Single backup for the whole reverted tx
633
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
634
+ this.backupFailedTx({
635
+ id: result.receipt.transactionHash,
636
+ failureType: 'revert',
637
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
638
+ blobData: txContext.blobData,
639
+ l1BlockNumber: result.receipt.blockNumber.toString(),
640
+ receipt: {
641
+ transactionHash: result.receipt.transactionHash,
642
+ blockNumber: result.receipt.blockNumber.toString(),
643
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
644
+ status: 'reverted',
645
+ },
646
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
647
+ context: {
648
+ actions: failedActions,
649
+ requests: requests
650
+ .filter(r => failedActions.includes(r.action))
651
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
652
+ sender: this.getSenderAddress().toString(),
653
+ },
654
+ });
655
+ }
413
656
  return { successfulActions, failedActions };
414
657
  }
415
658
  }
416
659
 
417
660
  /**
418
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
661
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
419
662
  * @param tipArchive - The archive to check
420
663
  * @returns The slot and block number if it is possible to propose, undefined otherwise
421
664
  */
422
- public canProposeAtNextEthBlock(
665
+ public canProposeAt(
423
666
  tipArchive: Fr,
424
667
  msgSender: EthAddress,
425
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
668
+ opts: {
669
+ forcePendingCheckpointNumber?: CheckpointNumber;
670
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
671
+ pipelined?: boolean;
672
+ } = {},
426
673
  ) {
427
674
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
428
675
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
429
676
 
677
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
678
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
679
+ const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
680
+
430
681
  return this.rollupContract
431
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
682
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
432
683
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
684
+ forceArchive: opts.forceArchive,
433
685
  })
434
686
  .catch(err => {
435
687
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -442,6 +694,7 @@ export class SequencerPublisher {
442
694
  return undefined;
443
695
  });
444
696
  }
697
+
445
698
  /**
446
699
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
447
700
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -465,7 +718,7 @@ export class SequencerPublisher {
465
718
  flags,
466
719
  ] as const;
467
720
 
468
- const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
721
+ const ts = this.getSimulationTimestamp(header.slotNumber);
469
722
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
470
723
  opts?.forcePendingCheckpointNumber,
471
724
  );
@@ -488,7 +741,7 @@ export class SequencerPublisher {
488
741
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
489
742
  from: MULTI_CALL_3_ADDRESS,
490
743
  },
491
- { time: ts + 1n },
744
+ { time: ts },
492
745
  stateOverrides,
493
746
  );
494
747
  this.log.debug(`Simulated validateHeader`);
@@ -521,6 +774,8 @@ export class SequencerPublisher {
521
774
  const request = this.buildInvalidateCheckpointRequest(validationResult);
522
775
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
523
776
 
777
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
778
+
524
779
  try {
525
780
  const { gasUsed } = await this.l1TxUtils.simulate(
526
781
  request,
@@ -539,6 +794,7 @@ export class SequencerPublisher {
539
794
  gasUsed,
540
795
  checkpointNumber,
541
796
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
797
+ lastArchive: validationResult.checkpoint.lastArchive,
542
798
  reason,
543
799
  };
544
800
  } catch (err) {
@@ -551,8 +807,8 @@ export class SequencerPublisher {
551
807
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
552
808
  { ...logData, request, error: viemError.message },
553
809
  );
554
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
555
- if (latestPendingCheckpointNumber < checkpointNumber) {
810
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
811
+ if (latestProposedCheckpointNumber < checkpointNumber) {
556
812
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
557
813
  return undefined;
558
814
  } else {
@@ -572,6 +828,18 @@ export class SequencerPublisher {
572
828
 
573
829
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
830
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
831
+ this.backupFailedTx({
832
+ id: keccak256(request.data!),
833
+ failureType: 'simulation',
834
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
835
+ l1BlockNumber: l1BlockNumber.toString(),
836
+ error: { message: viemError.message, name: viemError.name },
837
+ context: {
838
+ actions: [`invalidate-${reason}`],
839
+ checkpointNumber,
840
+ sender: this.getSenderAddress().toString(),
841
+ },
842
+ });
575
843
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
576
844
  }
577
845
  }
@@ -614,27 +882,13 @@ export class SequencerPublisher {
614
882
  checkpoint: Checkpoint,
615
883
  attestationsAndSigners: CommitteeAttestationsAndSigners,
616
884
  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
-
885
+ options: {
886
+ forcePendingCheckpointNumber?: CheckpointNumber;
887
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
888
+ },
889
+ ): Promise<void> {
636
890
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
891
+ const blobs = await getBlobsPerL1Block(blobFields);
638
892
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
893
 
640
894
  const args = [
@@ -642,7 +896,7 @@ export class SequencerPublisher {
642
896
  header: checkpoint.header.toViem(),
643
897
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
898
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
899
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
900
  },
647
901
  },
648
902
  attestationsAndSigners.getPackedAttestations(),
@@ -651,13 +905,11 @@ export class SequencerPublisher {
651
905
  blobInput,
652
906
  ] as const;
653
907
 
654
- await this.simulateProposeTx(args, ts, options);
655
- return ts;
908
+ await this.simulateProposeTx(args, options);
656
909
  }
657
910
 
658
911
  private async enqueueCastSignalHelper(
659
912
  slotNumber: SlotNumber,
660
- timestamp: bigint,
661
913
  signalType: GovernanceSignalAction,
662
914
  payload: EthAddress,
663
915
  base: IEmpireBase,
@@ -691,6 +943,32 @@ export class SequencerPublisher {
691
943
  return false;
692
944
  }
693
945
 
946
+ // Check if payload was already submitted to governance
947
+ const cacheKey = payload.toString();
948
+ if (!this.payloadProposedCache.has(cacheKey)) {
949
+ try {
950
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
951
+ const proposed = await retry(
952
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
953
+ 'Check if payload was proposed',
954
+ makeBackoff([0, 1, 2]),
955
+ this.log,
956
+ true,
957
+ );
958
+ if (proposed) {
959
+ this.payloadProposedCache.add(cacheKey);
960
+ }
961
+ } catch (err) {
962
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
963
+ return false;
964
+ }
965
+ }
966
+
967
+ if (this.payloadProposedCache.has(cacheKey)) {
968
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
969
+ return false;
970
+ }
971
+
694
972
  const cachedLastVote = this.lastActions[signalType];
695
973
  this.lastActions[signalType] = slotNumber;
696
974
  const action = signalType;
@@ -709,11 +987,30 @@ export class SequencerPublisher {
709
987
  lastValidL2Slot: slotNumber,
710
988
  });
711
989
 
990
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
991
+ const timestamp = this.getSimulationTimestamp(slotNumber);
992
+
712
993
  try {
713
994
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
995
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
996
  } catch (err) {
716
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
997
+ const viemError = formatViemError(err);
998
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
999
+ simulationTimestamp: timestamp,
1000
+ l1BlockNumber,
1001
+ });
1002
+ this.backupFailedTx({
1003
+ id: keccak256(request.data!),
1004
+ failureType: 'simulation',
1005
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1006
+ l1BlockNumber: l1BlockNumber.toString(),
1007
+ error: { message: viemError.message, name: viemError.name },
1008
+ context: {
1009
+ actions: [action],
1010
+ slot: slotNumber,
1011
+ sender: this.getSenderAddress().toString(),
1012
+ },
1013
+ });
717
1014
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
1015
  }
719
1016
 
@@ -764,19 +1061,16 @@ export class SequencerPublisher {
764
1061
  /**
765
1062
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
766
1063
  * @param slotNumber - The slot number to cast a signal for.
767
- * @param timestamp - The timestamp of the slot to cast a signal for.
768
1064
  * @returns True if the signal was successfully enqueued, false otherwise.
769
1065
  */
770
1066
  public enqueueGovernanceCastSignal(
771
1067
  governancePayload: EthAddress,
772
1068
  slotNumber: SlotNumber,
773
- timestamp: bigint,
774
1069
  signerAddress: EthAddress,
775
1070
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
776
1071
  ): Promise<boolean> {
777
1072
  return this.enqueueCastSignalHelper(
778
1073
  slotNumber,
779
- timestamp,
780
1074
  'governance-signal',
781
1075
  governancePayload,
782
1076
  this.govProposerContract,
@@ -789,7 +1083,6 @@ export class SequencerPublisher {
789
1083
  public async enqueueSlashingActions(
790
1084
  actions: ProposerSlashAction[],
791
1085
  slotNumber: SlotNumber,
792
- timestamp: bigint,
793
1086
  signerAddress: EthAddress,
794
1087
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
795
1088
  ): Promise<boolean> {
@@ -800,58 +1093,6 @@ export class SequencerPublisher {
800
1093
 
801
1094
  for (const action of actions) {
802
1095
  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
1096
  case 'vote-offenses': {
856
1097
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
857
1098
  slotNumber,
@@ -859,19 +1100,17 @@ export class SequencerPublisher {
859
1100
  votesCount: action.votes.length,
860
1101
  signerAddress,
861
1102
  });
862
- if (this.slashingProposerContract?.type !== 'tally') {
863
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1103
+ if (!this.slashingProposerContract) {
1104
+ this.log.error('No slashing proposer contract available');
864
1105
  return false;
865
1106
  }
866
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
867
1107
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
868
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1108
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
869
1109
  await this.simulateAndEnqueueRequest(
870
1110
  'vote-offenses',
871
1111
  request,
872
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1112
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
873
1113
  slotNumber,
874
- timestamp,
875
1114
  );
876
1115
  break;
877
1116
  }
@@ -882,18 +1121,20 @@ export class SequencerPublisher {
882
1121
  round: action.round,
883
1122
  signerAddress,
884
1123
  });
885
- if (this.slashingProposerContract?.type !== 'tally') {
886
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1124
+ if (!this.slashingProposerContract) {
1125
+ this.log.error('No slashing proposer contract available');
887
1126
  return false;
888
1127
  }
889
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
890
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1128
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1129
+ action.round,
1130
+ action.committees,
1131
+ );
891
1132
  await this.simulateAndEnqueueRequest(
892
1133
  'execute-slash',
893
- request,
894
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1134
+ executeRequest,
1135
+ (receipt: TransactionReceipt) =>
1136
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
895
1137
  slotNumber,
896
- timestamp,
897
1138
  );
898
1139
  break;
899
1140
  }
@@ -913,30 +1154,33 @@ export class SequencerPublisher {
913
1154
  checkpoint: Checkpoint,
914
1155
  attestationsAndSigners: CommitteeAttestationsAndSigners,
915
1156
  attestationsAndSignersSignature: Signature,
916
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1157
+ opts: {
1158
+ txTimeoutAt?: Date;
1159
+ forcePendingCheckpointNumber?: CheckpointNumber;
1160
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1161
+ } = {},
917
1162
  ): Promise<void> {
918
1163
  const checkpointHeader = checkpoint.header;
919
1164
 
920
1165
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
1166
+ const blobs = await getBlobsPerL1Block(blobFields);
922
1167
 
923
- const proposeTxArgs = {
1168
+ const proposeTxArgs: L1ProcessArgs = {
924
1169
  header: checkpointHeader,
925
1170
  archive: checkpoint.archive.root.toBuffer(),
926
1171
  blobs,
927
1172
  attestationsAndSigners,
928
1173
  attestationsAndSignersSignature,
1174
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
1175
  };
930
1176
 
931
- let ts: bigint;
932
-
933
1177
  try {
934
1178
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
935
1179
  // This means that we can avoid the simulation issues in later checks.
936
1180
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
937
1181
  // make time consistency checks break.
938
1182
  // 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(
1183
+ await this.validateCheckpointForSubmission(
940
1184
  checkpoint,
941
1185
  attestationsAndSigners,
942
1186
  attestationsAndSignersSignature,
@@ -951,8 +1195,26 @@ export class SequencerPublisher {
951
1195
  throw err;
952
1196
  }
953
1197
 
1198
+ // Build a pre-check callback that re-validates the checkpoint before L1 submission.
1199
+ // During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
1200
+ let preCheck = undefined;
1201
+ if (this.epochCache.isProposerPipeliningEnabled()) {
1202
+ preCheck = async () => {
1203
+ this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
1204
+ await this.validateCheckpointForSubmission(
1205
+ checkpoint,
1206
+ attestationsAndSigners,
1207
+ attestationsAndSignersSignature,
1208
+ {
1209
+ // Forcing pending checkpoint number is included its required if an invalidation request is included
1210
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1211
+ },
1212
+ );
1213
+ };
1214
+ }
1215
+
954
1216
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
955
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1217
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, preCheck);
956
1218
  }
957
1219
 
958
1220
  public enqueueInvalidateCheckpoint(
@@ -995,8 +1257,8 @@ export class SequencerPublisher {
995
1257
  request: L1TxRequest,
996
1258
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
997
1259
  slotNumber: SlotNumber,
998
- timestamp: bigint,
999
1260
  ) {
1261
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1000
1262
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1001
1263
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1002
1264
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1008,15 +1270,31 @@ export class SequencerPublisher {
1008
1270
 
1009
1271
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1010
1272
 
1273
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1274
+
1011
1275
  let gasUsed: bigint;
1012
1276
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1277
+
1013
1278
  try {
1014
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1279
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1015
1280
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1016
1281
  } catch (err) {
1017
1282
  const viemError = formatViemError(err, simulateAbi);
1018
1283
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
1284
 
1285
+ this.backupFailedTx({
1286
+ id: keccak256(request.data!),
1287
+ failureType: 'simulation',
1288
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1289
+ l1BlockNumber: l1BlockNumber.toString(),
1290
+ error: { message: viemError.message, name: viemError.name },
1291
+ context: {
1292
+ actions: [action],
1293
+ slot: slotNumber,
1294
+ sender: this.getSenderAddress().toString(),
1295
+ },
1296
+ });
1297
+
1020
1298
  return false;
1021
1299
  }
1022
1300
 
@@ -1056,6 +1334,7 @@ export class SequencerPublisher {
1056
1334
  */
1057
1335
  public interrupt() {
1058
1336
  this.interrupted = true;
1337
+ this.interruptibleSleep.interrupt();
1059
1338
  this.l1TxUtils.interrupt();
1060
1339
  }
1061
1340
 
@@ -1067,7 +1346,6 @@ export class SequencerPublisher {
1067
1346
 
1068
1347
  private async prepareProposeTx(
1069
1348
  encodedData: L1ProcessArgs,
1070
- timestamp: bigint,
1071
1349
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1072
1350
  ) {
1073
1351
  const kzg = Blob.getViemKzgInstance();
@@ -1100,9 +1378,27 @@ export class SequencerPublisher {
1100
1378
  kzg,
1101
1379
  },
1102
1380
  )
1103
- .catch(err => {
1104
- const { message, metaMessages } = formatViemError(err);
1105
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1381
+ .catch(async err => {
1382
+ const viemError = formatViemError(err);
1383
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1384
+ const validateBlobsData = encodeFunctionData({
1385
+ abi: RollupAbi,
1386
+ functionName: 'validateBlobs',
1387
+ args: [blobInput],
1388
+ });
1389
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1390
+ this.backupFailedTx({
1391
+ id: keccak256(validateBlobsData),
1392
+ failureType: 'simulation',
1393
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1394
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1395
+ l1BlockNumber: l1BlockNumber.toString(),
1396
+ error: { message: viemError.message, name: viemError.name },
1397
+ context: {
1398
+ actions: ['validate-blobs'],
1399
+ sender: this.getSenderAddress().toString(),
1400
+ },
1401
+ });
1106
1402
  throw new Error('Failed to validate blobs');
1107
1403
  });
1108
1404
  }
@@ -1113,8 +1409,7 @@ export class SequencerPublisher {
1113
1409
  header: encodedData.header.toViem(),
1114
1410
  archive: toHex(encodedData.archive),
1115
1411
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1412
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1413
  },
1119
1414
  },
1120
1415
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1123,7 +1418,7 @@ export class SequencerPublisher {
1123
1418
  blobInput,
1124
1419
  ] as const;
1125
1420
 
1126
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1421
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1127
1422
 
1128
1423
  return { args, blobEvaluationGas, rollupData, simulationResult };
1129
1424
  }
@@ -1131,7 +1426,6 @@ export class SequencerPublisher {
1131
1426
  /**
1132
1427
  * Simulates the propose tx with eth_simulateV1
1133
1428
  * @param args - The propose tx args
1134
- * @param timestamp - The timestamp to simulate proposal at
1135
1429
  * @returns The simulation result
1136
1430
  */
1137
1431
  private async simulateProposeTx(
@@ -1140,7 +1434,7 @@ export class SequencerPublisher {
1140
1434
  readonly header: ViemHeader;
1141
1435
  readonly archive: `0x${string}`;
1142
1436
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1437
+ readonly feeAssetPriceModifier: bigint;
1144
1438
  };
1145
1439
  },
1146
1440
  ViemCommitteeAttestations,
@@ -1148,8 +1442,10 @@ export class SequencerPublisher {
1148
1442
  ViemSignature,
1149
1443
  `0x${string}`,
1150
1444
  ],
1151
- timestamp: bigint,
1152
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1445
+ options: {
1446
+ forcePendingCheckpointNumber?: CheckpointNumber;
1447
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1448
+ },
1153
1449
  ) {
1154
1450
  const rollupData = encodeFunctionData({
1155
1451
  abi: RollupAbi,
@@ -1157,13 +1453,23 @@ export class SequencerPublisher {
1157
1453
  args,
1158
1454
  });
1159
1455
 
1160
- // override the pending checkpoint number if requested
1456
+ // override the proposed checkpoint number if requested
1161
1457
  const forcePendingCheckpointNumberStateDiff = (
1162
1458
  options.forcePendingCheckpointNumber !== undefined
1163
1459
  ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1164
1460
  : []
1165
1461
  ).flatMap(override => override.stateDiff ?? []);
1166
1462
 
1463
+ // override the fee header for a specific checkpoint number if requested (used when pipelining)
1464
+ const forceProposedFeeHeaderStateDiff = (
1465
+ options.forceProposedFeeHeader !== undefined
1466
+ ? await this.rollupContract.makeFeeHeaderOverride(
1467
+ options.forceProposedFeeHeader.checkpointNumber,
1468
+ options.forceProposedFeeHeader.feeHeader,
1469
+ )
1470
+ : []
1471
+ ).flatMap(override => override.stateDiff ?? []);
1472
+
1167
1473
  const stateOverrides: StateOverride = [
1168
1474
  {
1169
1475
  address: this.rollupContract.address,
@@ -1171,6 +1477,7 @@ export class SequencerPublisher {
1171
1477
  stateDiff: [
1172
1478
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1173
1479
  ...forcePendingCheckpointNumberStateDiff,
1480
+ ...forceProposedFeeHeaderStateDiff,
1174
1481
  ],
1175
1482
  },
1176
1483
  ];
@@ -1182,6 +1489,9 @@ export class SequencerPublisher {
1182
1489
  });
1183
1490
  }
1184
1491
 
1492
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1493
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1494
+
1185
1495
  const simulationResult = await this.l1TxUtils
1186
1496
  .simulate(
1187
1497
  {
@@ -1191,8 +1501,7 @@ export class SequencerPublisher {
1191
1501
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1192
1502
  },
1193
1503
  {
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,
1504
+ time: simTs,
1196
1505
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1197
1506
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1198
1507
  },
@@ -1214,7 +1523,19 @@ export class SequencerPublisher {
1214
1523
  logs: [],
1215
1524
  };
1216
1525
  }
1217
- this.log.error(`Failed to simulate propose tx`, viemError);
1526
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1527
+ this.backupFailedTx({
1528
+ id: keccak256(rollupData),
1529
+ failureType: 'simulation',
1530
+ request: { to: this.rollupContract.address, data: rollupData },
1531
+ l1BlockNumber: l1BlockNumber.toString(),
1532
+ error: { message: viemError.message, name: viemError.name },
1533
+ context: {
1534
+ actions: ['propose'],
1535
+ slot: Number(args[0].header.slotNumber),
1536
+ sender: this.getSenderAddress().toString(),
1537
+ },
1538
+ });
1218
1539
  throw err;
1219
1540
  });
1220
1541
 
@@ -1224,17 +1545,17 @@ export class SequencerPublisher {
1224
1545
  private async addProposeTx(
1225
1546
  checkpoint: Checkpoint,
1226
1547
  encodedData: L1ProcessArgs,
1227
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1228
- timestamp: bigint,
1548
+ opts: {
1549
+ txTimeoutAt?: Date;
1550
+ forcePendingCheckpointNumber?: CheckpointNumber;
1551
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1552
+ } = {},
1553
+ preCheck?: () => Promise<void>,
1229
1554
  ): Promise<void> {
1230
1555
  const slot = checkpoint.header.slotNumber;
1231
1556
  const timer = new Timer();
1232
1557
  const kzg = Blob.getViemKzgInstance();
1233
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1234
- encodedData,
1235
- timestamp,
1236
- opts,
1237
- );
1558
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1238
1559
  const startBlock = await this.l1TxUtils.getBlockNumber();
1239
1560
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1240
1561
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1258,6 +1579,7 @@ export class SequencerPublisher {
1258
1579
  },
1259
1580
  lastValidL2Slot: checkpoint.header.slotNumber,
1260
1581
  gasConfig: { ...opts, gasLimit },
1582
+ preCheck,
1261
1583
  blobConfig: {
1262
1584
  blobs: encodedData.blobs.map(b => b.data),
1263
1585
  kzg,
@@ -1310,4 +1632,17 @@ export class SequencerPublisher {
1310
1632
  },
1311
1633
  });
1312
1634
  }
1635
+
1636
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1637
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1638
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1639
+ const l1Constants = this.epochCache.getL1Constants();
1640
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1641
+ }
1642
+
1643
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1644
+ private getNextL1SlotTimestamp(): bigint {
1645
+ const l1Constants = this.epochCache.getL1Constants();
1646
+ return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);
1647
+ }
1313
1648
  }