@aztec/sequencer-client 0.0.1-commit.7ac86ea28 → 0.0.1-commit.7b86788

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 (34) hide show
  1. package/dest/publisher/config.d.ts +5 -1
  2. package/dest/publisher/config.d.ts.map +1 -1
  3. package/dest/publisher/config.js +6 -1
  4. package/dest/publisher/index.d.ts +2 -1
  5. package/dest/publisher/index.d.ts.map +1 -1
  6. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  7. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  8. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  9. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  10. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  11. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  12. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  13. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  14. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  15. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  16. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  17. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  18. package/dest/publisher/sequencer-publisher.d.ts +8 -2
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +217 -8
  21. package/dest/sequencer/checkpoint_proposal_job.d.ts +1 -1
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +8 -2
  24. package/dest/sequencer/timetable.js +1 -1
  25. package/package.json +28 -28
  26. package/src/publisher/config.ts +9 -0
  27. package/src/publisher/index.ts +3 -0
  28. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  29. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  30. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  31. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  32. package/src/publisher/sequencer-publisher.ts +178 -8
  33. package/src/sequencer/checkpoint_proposal_job.ts +14 -2
  34. package/src/sequencer/timetable.ts +1 -1
@@ -0,0 +1,46 @@
1
+ import { type Logger, createLogger } from '@aztec/foundation/log';
2
+ import type { FileStore } from '@aztec/stdlib/file-store';
3
+
4
+ import type { FailedL1Tx, FailedL1TxUri, L1TxFailedStore } from './failed_tx_store.js';
5
+
6
+ /**
7
+ * L1TxFailedStore implementation using the FileStore abstraction.
8
+ * Supports any backend that FileStore supports (GCS, S3, R2, local filesystem).
9
+ */
10
+ export class FileStoreL1TxFailedStore implements L1TxFailedStore {
11
+ private readonly log: Logger;
12
+
13
+ constructor(
14
+ private readonly fileStore: FileStore,
15
+ logger?: Logger,
16
+ ) {
17
+ this.log = logger ?? createLogger('sequencer:l1-tx-failed-store');
18
+ }
19
+
20
+ public async saveFailedTx(tx: FailedL1Tx): Promise<FailedL1TxUri> {
21
+ const prefix = tx.receipt ? 'tx' : 'data';
22
+ const path = `${tx.failureType}/${prefix}-${tx.id}.json`;
23
+ const json = JSON.stringify(tx, null, 2);
24
+
25
+ const uri = await this.fileStore.save(path, Buffer.from(json), {
26
+ metadata: {
27
+ 'content-type': 'application/json',
28
+ actions: tx.context.actions.join(','),
29
+ 'failure-type': tx.failureType,
30
+ },
31
+ });
32
+
33
+ this.log.info(`Saved failed L1 tx to ${uri}`, {
34
+ id: tx.id,
35
+ failureType: tx.failureType,
36
+ actions: tx.context.actions.join(','),
37
+ });
38
+
39
+ return uri as FailedL1TxUri;
40
+ }
41
+
42
+ public async getFailedTx(uri: FailedL1TxUri): Promise<FailedL1Tx> {
43
+ const data = await this.fileStore.read(uri);
44
+ return JSON.parse(data.toString()) as FailedL1Tx;
45
+ }
46
+ }
@@ -0,0 +1,3 @@
1
+ export { type FailedL1Tx, type FailedL1TxUri, type L1TxFailedStore } from './failed_tx_store.js';
2
+ export { createL1TxFailedStore } from './factory.js';
3
+ export { FileStoreL1TxFailedStore } from './file_store_failed_tx_store.js';
@@ -45,9 +45,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
45
45
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
46
46
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
47
47
 
48
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
48
+ import {
49
+ type Hex,
50
+ type StateOverride,
51
+ type TransactionReceipt,
52
+ type TypedDataDefinition,
53
+ encodeFunctionData,
54
+ keccak256,
55
+ multicall3Abi,
56
+ toHex,
57
+ } from 'viem';
49
58
 
50
59
  import type { SequencerPublisherConfig } from './config.js';
60
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
51
61
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
52
62
 
53
63
  /** Arguments to the process method of the rollup contract */
@@ -109,6 +119,7 @@ export class SequencerPublisher {
109
119
  private interrupted = false;
110
120
  private metrics: SequencerPublisherMetrics;
111
121
  public epochCache: EpochCache;
122
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
112
123
 
113
124
  protected governanceLog = createLogger('sequencer:publisher:governance');
114
125
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -149,7 +160,7 @@ export class SequencerPublisher {
149
160
  protected requests: RequestWithExpiry[] = [];
150
161
 
151
162
  constructor(
152
- private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
163
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
153
164
  Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
154
165
  deps: {
155
166
  telemetry?: TelemetryClient;
@@ -205,6 +216,31 @@ export class SequencerPublisher {
205
216
  this.rollupContract,
206
217
  createLogger('sequencer:publisher:price-oracle'),
207
218
  );
219
+
220
+ // Initialize failed L1 tx store (optional, for test networks)
221
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
222
+ }
223
+
224
+ /**
225
+ * Backs up a failed L1 transaction to the configured store for debugging.
226
+ * Does nothing if no store is configured.
227
+ */
228
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
229
+ if (!this.failedTxStore) {
230
+ return;
231
+ }
232
+
233
+ const tx: FailedL1Tx = {
234
+ ...failedTx,
235
+ timestamp: Date.now(),
236
+ };
237
+
238
+ // Fire and forget - don't block on backup
239
+ void this.failedTxStore
240
+ .then(store => store?.saveFailedTx(tx))
241
+ .catch(err => {
242
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
243
+ });
208
244
  }
209
245
 
210
246
  public getRollupContract(): RollupContract {
@@ -386,6 +422,21 @@ export class SequencerPublisher {
386
422
  validRequests.sort((a, b) => compareActions(a.action, b.action));
387
423
 
388
424
  try {
425
+ // Capture context for failed tx backup before sending
426
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
427
+ const multicallData = encodeFunctionData({
428
+ abi: multicall3Abi,
429
+ functionName: 'aggregate3',
430
+ args: [
431
+ validRequests.map(r => ({
432
+ target: r.request.to!,
433
+ callData: r.request.data!,
434
+ allowFailure: true,
435
+ })),
436
+ ],
437
+ });
438
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
439
+
389
440
  this.log.debug('Forwarding transactions', {
390
441
  validRequests: validRequests.map(request => request.action),
391
442
  txConfig,
@@ -398,7 +449,12 @@ export class SequencerPublisher {
398
449
  this.rollupContract.address,
399
450
  this.log,
400
451
  );
401
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
452
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
453
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
454
+ validRequests,
455
+ result,
456
+ txContext,
457
+ );
402
458
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
403
459
  } catch (err) {
404
460
  const viemError = formatViemError(err);
@@ -418,11 +474,25 @@ export class SequencerPublisher {
418
474
 
419
475
  private callbackBundledTransactions(
420
476
  requests: RequestWithExpiry[],
421
- result?: { receipt: TransactionReceipt } | FormattedViemError,
477
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
478
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
422
479
  ) {
423
480
  const actionsListStr = requests.map(r => r.action).join(', ');
424
481
  if (result instanceof FormattedViemError) {
425
482
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
483
+ this.backupFailedTx({
484
+ id: keccak256(txContext.multicallData),
485
+ failureType: 'send-error',
486
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
487
+ blobData: txContext.blobData,
488
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
489
+ error: { message: result.message, name: result.name },
490
+ context: {
491
+ actions: requests.map(r => r.action),
492
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
493
+ sender: this.getSenderAddress().toString(),
494
+ },
495
+ });
426
496
  return { failedActions: requests.map(r => r.action) };
427
497
  } else {
428
498
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -435,6 +505,30 @@ export class SequencerPublisher {
435
505
  failedActions.push(request.action);
436
506
  }
437
507
  }
508
+ // Single backup for the whole reverted tx
509
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
510
+ this.backupFailedTx({
511
+ id: result.receipt.transactionHash,
512
+ failureType: 'revert',
513
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
514
+ blobData: txContext.blobData,
515
+ l1BlockNumber: result.receipt.blockNumber.toString(),
516
+ receipt: {
517
+ transactionHash: result.receipt.transactionHash,
518
+ blockNumber: result.receipt.blockNumber.toString(),
519
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
520
+ status: 'reverted',
521
+ },
522
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
523
+ context: {
524
+ actions: failedActions,
525
+ requests: requests
526
+ .filter(r => failedActions.includes(r.action))
527
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
528
+ sender: this.getSenderAddress().toString(),
529
+ },
530
+ });
531
+ }
438
532
  return { successfulActions, failedActions };
439
533
  }
440
534
  }
@@ -546,6 +640,8 @@ export class SequencerPublisher {
546
640
  const request = this.buildInvalidateCheckpointRequest(validationResult);
547
641
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
548
642
 
643
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
644
+
549
645
  try {
550
646
  const { gasUsed } = await this.l1TxUtils.simulate(
551
647
  request,
@@ -597,6 +693,18 @@ export class SequencerPublisher {
597
693
 
598
694
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
599
695
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
696
+ this.backupFailedTx({
697
+ id: keccak256(request.data!),
698
+ failureType: 'simulation',
699
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
700
+ l1BlockNumber: l1BlockNumber.toString(),
701
+ error: { message: viemError.message, name: viemError.name },
702
+ context: {
703
+ actions: [`invalidate-${reason}`],
704
+ checkpointNumber,
705
+ sender: this.getSenderAddress().toString(),
706
+ },
707
+ });
600
708
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
601
709
  }
602
710
  }
@@ -744,11 +852,26 @@ export class SequencerPublisher {
744
852
  lastValidL2Slot: slotNumber,
745
853
  });
746
854
 
855
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
856
+
747
857
  try {
748
858
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
749
859
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
750
860
  } catch (err) {
751
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
861
+ const viemError = formatViemError(err);
862
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
863
+ this.backupFailedTx({
864
+ id: keccak256(request.data!),
865
+ failureType: 'simulation',
866
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
867
+ l1BlockNumber: l1BlockNumber.toString(),
868
+ error: { message: viemError.message, name: viemError.name },
869
+ context: {
870
+ actions: [action],
871
+ slot: slotNumber,
872
+ sender: this.getSenderAddress().toString(),
873
+ },
874
+ });
752
875
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
753
876
  }
754
877
 
@@ -1044,6 +1167,8 @@ export class SequencerPublisher {
1044
1167
 
1045
1168
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1046
1169
 
1170
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1171
+
1047
1172
  let gasUsed: bigint;
1048
1173
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1049
1174
  try {
@@ -1053,6 +1178,19 @@ export class SequencerPublisher {
1053
1178
  const viemError = formatViemError(err, simulateAbi);
1054
1179
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1055
1180
 
1181
+ this.backupFailedTx({
1182
+ id: keccak256(request.data!),
1183
+ failureType: 'simulation',
1184
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1185
+ l1BlockNumber: l1BlockNumber.toString(),
1186
+ error: { message: viemError.message, name: viemError.name },
1187
+ context: {
1188
+ actions: [action],
1189
+ slot: slotNumber,
1190
+ sender: this.getSenderAddress().toString(),
1191
+ },
1192
+ });
1193
+
1056
1194
  return false;
1057
1195
  }
1058
1196
 
@@ -1136,9 +1274,27 @@ export class SequencerPublisher {
1136
1274
  kzg,
1137
1275
  },
1138
1276
  )
1139
- .catch(err => {
1140
- const { message, metaMessages } = formatViemError(err);
1141
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1277
+ .catch(async err => {
1278
+ const viemError = formatViemError(err);
1279
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1280
+ const validateBlobsData = encodeFunctionData({
1281
+ abi: RollupAbi,
1282
+ functionName: 'validateBlobs',
1283
+ args: [blobInput],
1284
+ });
1285
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1286
+ this.backupFailedTx({
1287
+ id: keccak256(validateBlobsData),
1288
+ failureType: 'simulation',
1289
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1290
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1291
+ l1BlockNumber: l1BlockNumber.toString(),
1292
+ error: { message: viemError.message, name: viemError.name },
1293
+ context: {
1294
+ actions: ['validate-blobs'],
1295
+ sender: this.getSenderAddress().toString(),
1296
+ },
1297
+ });
1142
1298
  throw new Error('Failed to validate blobs');
1143
1299
  });
1144
1300
  }
@@ -1217,6 +1373,8 @@ export class SequencerPublisher {
1217
1373
  });
1218
1374
  }
1219
1375
 
1376
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1377
+
1220
1378
  const simulationResult = await this.l1TxUtils
1221
1379
  .simulate(
1222
1380
  {
@@ -1250,6 +1408,18 @@ export class SequencerPublisher {
1250
1408
  };
1251
1409
  }
1252
1410
  this.log.error(`Failed to simulate propose tx`, viemError);
1411
+ this.backupFailedTx({
1412
+ id: keccak256(rollupData),
1413
+ failureType: 'simulation',
1414
+ request: { to: this.rollupContract.address, data: rollupData },
1415
+ l1BlockNumber: l1BlockNumber.toString(),
1416
+ error: { message: viemError.message, name: viemError.name },
1417
+ context: {
1418
+ actions: ['propose'],
1419
+ slot: Number(args[0].header.slotNumber),
1420
+ sender: this.getSenderAddress().toString(),
1421
+ },
1422
+ });
1253
1423
  throw err;
1254
1424
  });
1255
1425
 
@@ -38,7 +38,7 @@ import {
38
38
  } from '@aztec/stdlib/interfaces/server';
39
39
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
40
40
  import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
41
- import { orderAttestations } from '@aztec/stdlib/p2p';
41
+ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
42
42
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
43
43
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
44
44
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
@@ -743,8 +743,20 @@ export class CheckpointProposalJob implements Traceable {
743
743
 
744
744
  collectedAttestationsCount = attestations.length;
745
745
 
746
+ // Trim attestations to minimum required to save L1 calldata gas
747
+ const localAddresses = this.validatorClient.getValidatorAddresses();
748
+ const trimmed = trimAttestations(
749
+ attestations,
750
+ numberOfRequiredAttestations,
751
+ this.attestorAddress,
752
+ localAddresses,
753
+ );
754
+ if (trimmed.length < attestations.length) {
755
+ this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
756
+ }
757
+
746
758
  // Rollup contract requires that the signatures are provided in the order of the committee
747
- const sorted = orderAttestations(attestations, committee);
759
+ const sorted = orderAttestations(trimmed, committee);
748
760
 
749
761
  // Manipulate the attestations if we've been configured to do so
750
762
  if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
@@ -132,7 +132,7 @@ export class SequencerTimetable {
132
132
  const initializeDeadline = this.aztecSlotDuration - minWorkToDo;
133
133
  this.initializeDeadline = initializeDeadline;
134
134
 
135
- this.log.verbose(
135
+ this.log.info(
136
136
  `Sequencer timetable initialized with ${this.maxNumberOfBlocks} blocks per slot (${this.enforce ? 'enforced' : 'not enforced'})`,
137
137
  {
138
138
  ethereumSlotDuration: this.ethereumSlotDuration,