@aztec/sequencer-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ee6fcc6

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