@aztec/sequencer-client 0.0.1-commit.8afd444 → 0.0.1-commit.8ee97c858

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 (77) hide show
  1. package/dest/client/sequencer-client.d.ts +12 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +56 -17
  4. package/dest/config.d.ts +26 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +47 -28
  7. package/dest/global_variable_builder/global_builder.d.ts +2 -4
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +5 -4
  10. package/dest/publisher/config.d.ts +35 -17
  11. package/dest/publisher/config.d.ts.map +1 -1
  12. package/dest/publisher/config.js +106 -42
  13. package/dest/publisher/index.d.ts +2 -1
  14. package/dest/publisher/index.d.ts.map +1 -1
  15. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  16. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  17. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  18. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  19. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  21. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  22. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  24. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  25. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  26. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  27. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  28. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher-factory.js +27 -2
  30. package/dest/publisher/sequencer-publisher.d.ts +30 -9
  31. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  32. package/dest/publisher/sequencer-publisher.js +323 -38
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts +15 -7
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +240 -139
  36. package/dest/sequencer/events.d.ts +2 -1
  37. package/dest/sequencer/events.d.ts.map +1 -1
  38. package/dest/sequencer/metrics.d.ts +21 -5
  39. package/dest/sequencer/metrics.d.ts.map +1 -1
  40. package/dest/sequencer/metrics.js +97 -15
  41. package/dest/sequencer/sequencer.d.ts +28 -15
  42. package/dest/sequencer/sequencer.d.ts.map +1 -1
  43. package/dest/sequencer/sequencer.js +91 -82
  44. package/dest/sequencer/timetable.d.ts +4 -6
  45. package/dest/sequencer/timetable.d.ts.map +1 -1
  46. package/dest/sequencer/timetable.js +7 -11
  47. package/dest/sequencer/types.d.ts +2 -2
  48. package/dest/sequencer/types.d.ts.map +1 -1
  49. package/dest/test/index.d.ts +3 -5
  50. package/dest/test/index.d.ts.map +1 -1
  51. package/dest/test/mock_checkpoint_builder.d.ts +11 -11
  52. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  53. package/dest/test/mock_checkpoint_builder.js +45 -34
  54. package/dest/test/utils.d.ts +3 -3
  55. package/dest/test/utils.d.ts.map +1 -1
  56. package/dest/test/utils.js +5 -4
  57. package/package.json +27 -28
  58. package/src/client/sequencer-client.ts +77 -18
  59. package/src/config.ts +65 -38
  60. package/src/global_variable_builder/global_builder.ts +4 -3
  61. package/src/publisher/config.ts +121 -43
  62. package/src/publisher/index.ts +3 -0
  63. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  64. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  65. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  66. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  67. package/src/publisher/sequencer-publisher-factory.ts +38 -6
  68. package/src/publisher/sequencer-publisher.ts +327 -52
  69. package/src/sequencer/checkpoint_proposal_job.ts +327 -150
  70. package/src/sequencer/events.ts +1 -1
  71. package/src/sequencer/metrics.ts +106 -18
  72. package/src/sequencer/sequencer.ts +125 -94
  73. package/src/sequencer/timetable.ts +13 -12
  74. package/src/sequencer/types.ts +1 -1
  75. package/src/test/index.ts +2 -4
  76. package/src/test/mock_checkpoint_builder.ts +63 -49
  77. package/src/test/utils.ts +5 -2
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
+ FeeAssetPriceOracle,
7
8
  type GovernanceProposerContract,
8
9
  type IEmpireBase,
9
10
  MULTI_CALL_3_ADDRESS,
@@ -18,20 +19,23 @@ 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';
35
39
  import { bufferToHex } from '@aztec/foundation/string';
36
40
  import { DateProvider, Timer } from '@aztec/foundation/timer';
37
41
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -43,9 +47,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
43
47
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
44
48
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
45
49
 
46
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
-
48
- import type { PublisherConfig, TxSenderConfig } from './config.js';
50
+ import {
51
+ type Hex,
52
+ type StateOverride,
53
+ type TransactionReceipt,
54
+ type TypedDataDefinition,
55
+ encodeFunctionData,
56
+ keccak256,
57
+ multicall3Abi,
58
+ toHex,
59
+ } from 'viem';
60
+
61
+ import type { SequencerPublisherConfig } from './config.js';
62
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
49
63
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
64
 
51
65
  /** Arguments to the process method of the rollup contract */
@@ -60,6 +74,8 @@ type L1ProcessArgs = {
60
74
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
75
  /** Attestations and signers signature */
62
76
  attestationsAndSignersSignature: Signature;
77
+ /** The fee asset price modifier in basis points (from oracle) */
78
+ feeAssetPriceModifier: bigint;
63
79
  };
64
80
 
65
81
  export const Actions = [
@@ -105,6 +121,7 @@ export class SequencerPublisher {
105
121
  private interrupted = false;
106
122
  private metrics: SequencerPublisherMetrics;
107
123
  public epochCache: EpochCache;
124
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
108
125
 
109
126
  protected governanceLog = createLogger('sequencer:publisher:governance');
110
127
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -112,24 +129,33 @@ export class SequencerPublisher {
112
129
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
130
 
114
131
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
132
+ private payloadProposedCache: Set<string> = new Set<string>();
115
133
 
116
134
  protected log: Logger;
117
135
  protected ethereumSlotDuration: bigint;
136
+ protected aztecSlotDuration: bigint;
118
137
 
119
138
  private blobClient: BlobClientInterface;
120
139
 
121
140
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
122
141
  private proposerAddressForSimulation?: EthAddress;
123
142
 
143
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
144
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
145
+
124
146
  /** L1 fee analyzer for fisherman mode */
125
147
  private l1FeeAnalyzer?: L1FeeAnalyzer;
148
+
149
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
150
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
151
+
126
152
  // A CALL to a cold address is 2700 gas
127
153
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
154
 
129
155
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
156
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
157
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
158
+ public l1TxUtils: L1TxUtils;
133
159
  public rollupContract: RollupContract;
134
160
  public govProposerContract: GovernanceProposerContract;
135
161
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -140,11 +166,12 @@ export class SequencerPublisher {
140
166
  protected requests: RequestWithExpiry[] = [];
141
167
 
142
168
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
169
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
170
+ Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'> & { l1ChainId: number },
144
171
  deps: {
145
172
  telemetry?: TelemetryClient;
146
173
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
174
+ l1TxUtils: L1TxUtils;
148
175
  rollupContract: RollupContract;
149
176
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
150
177
  governanceProposerContract: GovernanceProposerContract;
@@ -154,10 +181,12 @@ export class SequencerPublisher {
154
181
  metrics: SequencerPublisherMetrics;
155
182
  lastActions: Partial<Record<Action, SlotNumber>>;
156
183
  log?: Logger;
184
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
157
185
  },
158
186
  ) {
159
187
  this.log = deps.log ?? createLogger('sequencer:publisher');
160
188
  this.ethereumSlotDuration = BigInt(config.ethereumSlotDuration);
189
+ this.aztecSlotDuration = BigInt(config.aztecSlotDuration);
161
190
  this.epochCache = deps.epochCache;
162
191
  this.lastActions = deps.lastActions;
163
192
 
@@ -167,6 +196,7 @@ export class SequencerPublisher {
167
196
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
168
197
  this.tracer = telemetry.getTracer('SequencerPublisher');
169
198
  this.l1TxUtils = deps.l1TxUtils;
199
+ this.getNextPublisher = deps.getNextPublisher;
170
200
 
171
201
  this.rollupContract = deps.rollupContract;
172
202
 
@@ -188,12 +218,52 @@ export class SequencerPublisher {
188
218
  createLogger('sequencer:publisher:fee-analyzer'),
189
219
  );
190
220
  }
221
+
222
+ // Initialize fee asset price oracle
223
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
224
+ this.l1TxUtils.client,
225
+ this.rollupContract,
226
+ createLogger('sequencer:publisher:price-oracle'),
227
+ );
228
+
229
+ // Initialize failed L1 tx store (optional, for test networks)
230
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
231
+ }
232
+
233
+ /**
234
+ * Backs up a failed L1 transaction to the configured store for debugging.
235
+ * Does nothing if no store is configured.
236
+ */
237
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
238
+ if (!this.failedTxStore) {
239
+ return;
240
+ }
241
+
242
+ const tx: FailedL1Tx = {
243
+ ...failedTx,
244
+ timestamp: Date.now(),
245
+ };
246
+
247
+ // Fire and forget - don't block on backup
248
+ void this.failedTxStore
249
+ .then(store => store?.saveFailedTx(tx))
250
+ .catch(err => {
251
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
252
+ });
191
253
  }
192
254
 
193
255
  public getRollupContract(): RollupContract {
194
256
  return this.rollupContract;
195
257
  }
196
258
 
259
+ /**
260
+ * Gets the fee asset price modifier from the oracle.
261
+ * Returns 0n if the oracle query fails.
262
+ */
263
+ public getFeeAssetPriceModifier(): Promise<bigint> {
264
+ return this.feeAssetPriceOracle.computePriceModifier();
265
+ }
266
+
197
267
  public getSenderAddress() {
198
268
  return this.l1TxUtils.getSenderAddress();
199
269
  }
@@ -218,7 +288,7 @@ export class SequencerPublisher {
218
288
  }
219
289
 
220
290
  public getCurrentL2Slot(): SlotNumber {
221
- return this.epochCache.getEpochAndSlotNow().slot;
291
+ return this.epochCache.getSlotNow();
222
292
  }
223
293
 
224
294
  /**
@@ -331,8 +401,8 @@ export class SequencerPublisher {
331
401
  // @note - we can only have one blob config per bundle
332
402
  // find requests with gas and blob configs
333
403
  // 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);
404
+ const gasConfigs = validRequests.filter(request => request.gasConfig).map(request => request.gasConfig);
405
+ const blobConfigs = validRequests.filter(request => request.blobConfig).map(request => request.blobConfig);
336
406
 
337
407
  if (blobConfigs.length > 1) {
338
408
  throw new Error('Multiple blob configs found');
@@ -361,19 +431,36 @@ export class SequencerPublisher {
361
431
  validRequests.sort((a, b) => compareActions(a.action, b.action));
362
432
 
363
433
  try {
434
+ // Capture context for failed tx backup before sending
435
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
436
+ const multicallData = encodeFunctionData({
437
+ abi: multicall3Abi,
438
+ functionName: 'aggregate3',
439
+ args: [
440
+ validRequests.map(r => ({
441
+ target: r.request.to!,
442
+ callData: r.request.data!,
443
+ allowFailure: true,
444
+ })),
445
+ ],
446
+ });
447
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
448
+
449
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
450
+
364
451
  this.log.debug('Forwarding transactions', {
365
452
  validRequests: validRequests.map(request => request.action),
366
453
  txConfig,
367
454
  });
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,
455
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
456
+ if (result === undefined) {
457
+ return undefined;
458
+ }
459
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
460
+ validRequests,
461
+ result,
462
+ txContext,
375
463
  );
376
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
377
464
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
378
465
  } catch (err) {
379
466
  const viemError = formatViemError(err);
@@ -391,16 +478,88 @@ export class SequencerPublisher {
391
478
  }
392
479
  }
393
480
 
481
+ /**
482
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
483
+ * failure occurs (i.e. the tx never reached the chain).
484
+ * On-chain reverts and simulation errors are returned as-is without rotation.
485
+ */
486
+ private async forwardWithPublisherRotation(
487
+ validRequests: RequestWithExpiry[],
488
+ txConfig: RequestWithExpiry['gasConfig'],
489
+ blobConfig: L1BlobInputs | undefined,
490
+ ) {
491
+ const triedAddresses: EthAddress[] = [];
492
+ let currentPublisher = this.l1TxUtils;
493
+
494
+ while (true) {
495
+ triedAddresses.push(currentPublisher.getSenderAddress());
496
+ try {
497
+ const result = await Multicall3.forward(
498
+ validRequests.map(r => r.request),
499
+ currentPublisher,
500
+ txConfig,
501
+ blobConfig,
502
+ this.rollupContract.address,
503
+ this.log,
504
+ );
505
+ this.l1TxUtils = currentPublisher;
506
+ return result;
507
+ } catch (err) {
508
+ if (err instanceof TimeoutError) {
509
+ throw err;
510
+ }
511
+ const viemError = formatViemError(err);
512
+ if (!this.getNextPublisher) {
513
+ this.log.error('Failed to publish bundled transactions', viemError);
514
+ return undefined;
515
+ }
516
+ this.log.warn(
517
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
518
+ viemError,
519
+ );
520
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
521
+ if (!nextPublisher) {
522
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
523
+ return undefined;
524
+ }
525
+ currentPublisher = nextPublisher;
526
+ }
527
+ }
528
+ }
529
+
394
530
  private callbackBundledTransactions(
395
531
  requests: RequestWithExpiry[],
396
- result?: { receipt: TransactionReceipt } | FormattedViemError,
532
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
533
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
397
534
  ) {
398
535
  const actionsListStr = requests.map(r => r.action).join(', ');
399
536
  if (result instanceof FormattedViemError) {
400
537
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
538
+ this.backupFailedTx({
539
+ id: keccak256(txContext.multicallData),
540
+ failureType: 'send-error',
541
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
542
+ blobData: txContext.blobData,
543
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
544
+ error: { message: result.message, name: result.name },
545
+ context: {
546
+ actions: requests.map(r => r.action),
547
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
548
+ sender: this.getSenderAddress().toString(),
549
+ },
550
+ });
401
551
  return { failedActions: requests.map(r => r.action) };
402
552
  } else {
403
- this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
553
+ this.log.verbose(`Published bundled transactions (${actionsListStr})`, {
554
+ result,
555
+ requests: requests.map(r => ({
556
+ ...r,
557
+ // Avoid logging large blob data
558
+ blobConfig: r.blobConfig
559
+ ? { ...r.blobConfig, blobs: r.blobConfig.blobs.map(b => ({ size: trimmedBytesLength(b) })) }
560
+ : undefined,
561
+ })),
562
+ });
404
563
  const successfulActions: Action[] = [];
405
564
  const failedActions: Action[] = [];
406
565
  for (const request of requests) {
@@ -410,25 +569,52 @@ export class SequencerPublisher {
410
569
  failedActions.push(request.action);
411
570
  }
412
571
  }
572
+ // Single backup for the whole reverted tx
573
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
574
+ this.backupFailedTx({
575
+ id: result.receipt.transactionHash,
576
+ failureType: 'revert',
577
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
578
+ blobData: txContext.blobData,
579
+ l1BlockNumber: result.receipt.blockNumber.toString(),
580
+ receipt: {
581
+ transactionHash: result.receipt.transactionHash,
582
+ blockNumber: result.receipt.blockNumber.toString(),
583
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
584
+ status: 'reverted',
585
+ },
586
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
587
+ context: {
588
+ actions: failedActions,
589
+ requests: requests
590
+ .filter(r => failedActions.includes(r.action))
591
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
592
+ sender: this.getSenderAddress().toString(),
593
+ },
594
+ });
595
+ }
413
596
  return { successfulActions, failedActions };
414
597
  }
415
598
  }
416
599
 
417
600
  /**
418
- * @notice Will call `canProposeAtNextEthBlock` to make sure that it is possible to propose
601
+ * @notice Will call `canProposeAt` to make sure that it is possible to propose
419
602
  * @param tipArchive - The archive to check
420
603
  * @returns The slot and block number if it is possible to propose, undefined otherwise
421
604
  */
422
- public canProposeAtNextEthBlock(
605
+ public canProposeAt(
423
606
  tipArchive: Fr,
424
607
  msgSender: EthAddress,
425
- opts: { forcePendingCheckpointNumber?: CheckpointNumber } = {},
608
+ opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
426
609
  ) {
427
610
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
428
611
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
429
612
 
613
+ const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
614
+ const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
615
+
430
616
  return this.rollupContract
431
- .canProposeAtNextEthBlock(tipArchive.toBuffer(), msgSender.toString(), Number(this.ethereumSlotDuration), {
617
+ .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), this.ethereumSlotDuration, slotOffset, {
432
618
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
433
619
  })
434
620
  .catch(err => {
@@ -442,6 +628,7 @@ export class SequencerPublisher {
442
628
  return undefined;
443
629
  });
444
630
  }
631
+
445
632
  /**
446
633
  * @notice Will simulate `validateHeader` to make sure that the block header is valid
447
634
  * @dev This is a convenience function that can be used by the sequencer to validate a "partial" header.
@@ -521,6 +708,8 @@ export class SequencerPublisher {
521
708
  const request = this.buildInvalidateCheckpointRequest(validationResult);
522
709
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
523
710
 
711
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
712
+
524
713
  try {
525
714
  const { gasUsed } = await this.l1TxUtils.simulate(
526
715
  request,
@@ -572,6 +761,18 @@ export class SequencerPublisher {
572
761
 
573
762
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
574
763
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
764
+ this.backupFailedTx({
765
+ id: keccak256(request.data!),
766
+ failureType: 'simulation',
767
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
768
+ l1BlockNumber: l1BlockNumber.toString(),
769
+ error: { message: viemError.message, name: viemError.name },
770
+ context: {
771
+ actions: [`invalidate-${reason}`],
772
+ checkpointNumber,
773
+ sender: this.getSenderAddress().toString(),
774
+ },
775
+ });
575
776
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
576
777
  }
577
778
  }
@@ -616,25 +817,11 @@ export class SequencerPublisher {
616
817
  attestationsAndSignersSignature: Signature,
617
818
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
618
819
  ): 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
-
820
+ // Anchor the simulation timestamp to the checkpoint's own slot start time
821
+ // rather than the current L1 block timestamp, which may overshoot into the next slot if the build ran late.
822
+ const ts = checkpoint.header.timestamp;
636
823
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
824
+ const blobs = await getBlobsPerL1Block(blobFields);
638
825
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
826
 
640
827
  const args = [
@@ -642,7 +829,7 @@ export class SequencerPublisher {
642
829
  header: checkpoint.header.toViem(),
643
830
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
831
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
832
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
833
  },
647
834
  },
648
835
  attestationsAndSigners.getPackedAttestations(),
@@ -691,6 +878,32 @@ export class SequencerPublisher {
691
878
  return false;
692
879
  }
693
880
 
881
+ // Check if payload was already submitted to governance
882
+ const cacheKey = payload.toString();
883
+ if (!this.payloadProposedCache.has(cacheKey)) {
884
+ try {
885
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
886
+ const proposed = await retry(
887
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
888
+ 'Check if payload was proposed',
889
+ makeBackoff([0, 1, 2]),
890
+ this.log,
891
+ true,
892
+ );
893
+ if (proposed) {
894
+ this.payloadProposedCache.add(cacheKey);
895
+ }
896
+ } catch (err) {
897
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
898
+ return false;
899
+ }
900
+ }
901
+
902
+ if (this.payloadProposedCache.has(cacheKey)) {
903
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
904
+ return false;
905
+ }
906
+
694
907
  const cachedLastVote = this.lastActions[signalType];
695
908
  this.lastActions[signalType] = slotNumber;
696
909
  const action = signalType;
@@ -709,11 +922,26 @@ export class SequencerPublisher {
709
922
  lastValidL2Slot: slotNumber,
710
923
  });
711
924
 
925
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
926
+
712
927
  try {
713
928
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
714
929
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
715
930
  } catch (err) {
716
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
931
+ const viemError = formatViemError(err);
932
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
933
+ this.backupFailedTx({
934
+ id: keccak256(request.data!),
935
+ failureType: 'simulation',
936
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
937
+ l1BlockNumber: l1BlockNumber.toString(),
938
+ error: { message: viemError.message, name: viemError.name },
939
+ context: {
940
+ actions: [action],
941
+ slot: slotNumber,
942
+ sender: this.getSenderAddress().toString(),
943
+ },
944
+ });
717
945
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
718
946
  }
719
947
 
@@ -918,14 +1146,15 @@ export class SequencerPublisher {
918
1146
  const checkpointHeader = checkpoint.header;
919
1147
 
920
1148
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
1149
+ const blobs = await getBlobsPerL1Block(blobFields);
922
1150
 
923
- const proposeTxArgs = {
1151
+ const proposeTxArgs: L1ProcessArgs = {
924
1152
  header: checkpointHeader,
925
1153
  archive: checkpoint.archive.root.toBuffer(),
926
1154
  blobs,
927
1155
  attestationsAndSigners,
928
1156
  attestationsAndSignersSignature,
1157
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
1158
  };
930
1159
 
931
1160
  let ts: bigint;
@@ -1008,6 +1237,8 @@ export class SequencerPublisher {
1008
1237
 
1009
1238
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1010
1239
 
1240
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1241
+
1011
1242
  let gasUsed: bigint;
1012
1243
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1013
1244
  try {
@@ -1017,6 +1248,19 @@ export class SequencerPublisher {
1017
1248
  const viemError = formatViemError(err, simulateAbi);
1018
1249
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1019
1250
 
1251
+ this.backupFailedTx({
1252
+ id: keccak256(request.data!),
1253
+ failureType: 'simulation',
1254
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1255
+ l1BlockNumber: l1BlockNumber.toString(),
1256
+ error: { message: viemError.message, name: viemError.name },
1257
+ context: {
1258
+ actions: [action],
1259
+ slot: slotNumber,
1260
+ sender: this.getSenderAddress().toString(),
1261
+ },
1262
+ });
1263
+
1020
1264
  return false;
1021
1265
  }
1022
1266
 
@@ -1100,9 +1344,27 @@ export class SequencerPublisher {
1100
1344
  kzg,
1101
1345
  },
1102
1346
  )
1103
- .catch(err => {
1104
- const { message, metaMessages } = formatViemError(err);
1105
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1347
+ .catch(async err => {
1348
+ const viemError = formatViemError(err);
1349
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1350
+ const validateBlobsData = encodeFunctionData({
1351
+ abi: RollupAbi,
1352
+ functionName: 'validateBlobs',
1353
+ args: [blobInput],
1354
+ });
1355
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1356
+ this.backupFailedTx({
1357
+ id: keccak256(validateBlobsData),
1358
+ failureType: 'simulation',
1359
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1360
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1361
+ l1BlockNumber: l1BlockNumber.toString(),
1362
+ error: { message: viemError.message, name: viemError.name },
1363
+ context: {
1364
+ actions: ['validate-blobs'],
1365
+ sender: this.getSenderAddress().toString(),
1366
+ },
1367
+ });
1106
1368
  throw new Error('Failed to validate blobs');
1107
1369
  });
1108
1370
  }
@@ -1113,8 +1375,7 @@ export class SequencerPublisher {
1113
1375
  header: encodedData.header.toViem(),
1114
1376
  archive: toHex(encodedData.archive),
1115
1377
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1378
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1379
  },
1119
1380
  },
1120
1381
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1140,7 +1401,7 @@ export class SequencerPublisher {
1140
1401
  readonly header: ViemHeader;
1141
1402
  readonly archive: `0x${string}`;
1142
1403
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1404
+ readonly feeAssetPriceModifier: bigint;
1144
1405
  };
1145
1406
  },
1146
1407
  ViemCommitteeAttestations,
@@ -1182,6 +1443,8 @@ export class SequencerPublisher {
1182
1443
  });
1183
1444
  }
1184
1445
 
1446
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1447
+
1185
1448
  const simulationResult = await this.l1TxUtils
1186
1449
  .simulate(
1187
1450
  {
@@ -1215,6 +1478,18 @@ export class SequencerPublisher {
1215
1478
  };
1216
1479
  }
1217
1480
  this.log.error(`Failed to simulate propose tx`, viemError);
1481
+ this.backupFailedTx({
1482
+ id: keccak256(rollupData),
1483
+ failureType: 'simulation',
1484
+ request: { to: this.rollupContract.address, data: rollupData },
1485
+ l1BlockNumber: l1BlockNumber.toString(),
1486
+ error: { message: viemError.message, name: viemError.name },
1487
+ context: {
1488
+ actions: ['propose'],
1489
+ slot: Number(args[0].header.slotNumber),
1490
+ sender: this.getSenderAddress().toString(),
1491
+ },
1492
+ });
1218
1493
  throw err;
1219
1494
  });
1220
1495