@aztec/sequencer-client 0.0.1-commit.4eabbdb → 0.0.1-commit.5358163d3

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 (46) hide show
  1. package/dest/config.d.ts +2 -2
  2. package/dest/config.d.ts.map +1 -1
  3. package/dest/config.js +13 -8
  4. package/dest/publisher/config.d.ts +5 -1
  5. package/dest/publisher/config.d.ts.map +1 -1
  6. package/dest/publisher/config.js +6 -1
  7. package/dest/publisher/index.d.ts +2 -1
  8. package/dest/publisher/index.d.ts.map +1 -1
  9. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  10. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  11. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  12. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  13. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  14. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  15. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  16. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  17. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  18. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  19. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  20. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  21. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -1
  22. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher-factory.js +14 -0
  24. package/dest/publisher/sequencer-publisher.d.ts +12 -2
  25. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher.js +258 -9
  27. package/dest/sequencer/checkpoint_proposal_job.d.ts +1 -1
  28. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  29. package/dest/sequencer/checkpoint_proposal_job.js +40 -15
  30. package/dest/sequencer/sequencer.d.ts +8 -2
  31. package/dest/sequencer/sequencer.d.ts.map +1 -1
  32. package/dest/sequencer/sequencer.js +6 -1
  33. package/dest/sequencer/timetable.js +1 -1
  34. package/package.json +28 -28
  35. package/src/config.ts +14 -8
  36. package/src/publisher/config.ts +9 -0
  37. package/src/publisher/index.ts +3 -0
  38. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  39. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  40. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  41. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  42. package/src/publisher/sequencer-publisher-factory.ts +15 -0
  43. package/src/publisher/sequencer-publisher.ts +237 -15
  44. package/src/sequencer/checkpoint_proposal_job.ts +58 -11
  45. package/src/sequencer/sequencer.ts +8 -1
  46. package/src/sequencer/timetable.ts +1 -1
@@ -30,6 +30,7 @@ import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
30
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
31
31
  import { pick } from '@aztec/foundation/collection';
32
32
  import type { Fr } from '@aztec/foundation/curves/bn254';
33
+ import { TimeoutError } from '@aztec/foundation/error';
33
34
  import { EthAddress } from '@aztec/foundation/eth-address';
34
35
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
36
  import { type Logger, createLogger } from '@aztec/foundation/log';
@@ -45,9 +46,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
45
46
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
46
47
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
47
48
 
48
- import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
49
+ import {
50
+ type Hex,
51
+ type StateOverride,
52
+ type TransactionReceipt,
53
+ type TypedDataDefinition,
54
+ encodeFunctionData,
55
+ keccak256,
56
+ multicall3Abi,
57
+ toHex,
58
+ } from 'viem';
49
59
 
50
60
  import type { SequencerPublisherConfig } from './config.js';
61
+ import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
51
62
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
52
63
 
53
64
  /** Arguments to the process method of the rollup contract */
@@ -109,6 +120,7 @@ export class SequencerPublisher {
109
120
  private interrupted = false;
110
121
  private metrics: SequencerPublisherMetrics;
111
122
  public epochCache: EpochCache;
123
+ private failedTxStore?: Promise<L1TxFailedStore | undefined>;
112
124
 
113
125
  protected governanceLog = createLogger('sequencer:publisher:governance');
114
126
  protected slashingLog = createLogger('sequencer:publisher:slashing');
@@ -126,6 +138,9 @@ export class SequencerPublisher {
126
138
  /** Address to use for simulations in fisherman mode (actual proposer's address) */
127
139
  private proposerAddressForSimulation?: EthAddress;
128
140
 
141
+ /** Optional callback to obtain a replacement publisher when the current one fails to send. */
142
+ private getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
143
+
129
144
  /** L1 fee analyzer for fisherman mode */
130
145
  private l1FeeAnalyzer?: L1FeeAnalyzer;
131
146
 
@@ -149,7 +164,7 @@ export class SequencerPublisher {
149
164
  protected requests: RequestWithExpiry[] = [];
150
165
 
151
166
  constructor(
152
- private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
167
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode' | 'l1TxFailedStore'> &
153
168
  Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
154
169
  deps: {
155
170
  telemetry?: TelemetryClient;
@@ -164,6 +179,7 @@ export class SequencerPublisher {
164
179
  metrics: SequencerPublisherMetrics;
165
180
  lastActions: Partial<Record<Action, SlotNumber>>;
166
181
  log?: Logger;
182
+ getNextPublisher?: (excludeAddresses: EthAddress[]) => Promise<L1TxUtils | undefined>;
167
183
  },
168
184
  ) {
169
185
  this.log = deps.log ?? createLogger('sequencer:publisher');
@@ -177,6 +193,7 @@ export class SequencerPublisher {
177
193
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
178
194
  this.tracer = telemetry.getTracer('SequencerPublisher');
179
195
  this.l1TxUtils = deps.l1TxUtils;
196
+ this.getNextPublisher = deps.getNextPublisher;
180
197
 
181
198
  this.rollupContract = deps.rollupContract;
182
199
 
@@ -205,6 +222,31 @@ export class SequencerPublisher {
205
222
  this.rollupContract,
206
223
  createLogger('sequencer:publisher:price-oracle'),
207
224
  );
225
+
226
+ // Initialize failed L1 tx store (optional, for test networks)
227
+ this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log);
228
+ }
229
+
230
+ /**
231
+ * Backs up a failed L1 transaction to the configured store for debugging.
232
+ * Does nothing if no store is configured.
233
+ */
234
+ private backupFailedTx(failedTx: Omit<FailedL1Tx, 'timestamp'>): void {
235
+ if (!this.failedTxStore) {
236
+ return;
237
+ }
238
+
239
+ const tx: FailedL1Tx = {
240
+ ...failedTx,
241
+ timestamp: Date.now(),
242
+ };
243
+
244
+ // Fire and forget - don't block on backup
245
+ void this.failedTxStore
246
+ .then(store => store?.saveFailedTx(tx))
247
+ .catch(err => {
248
+ this.log.warn(`Failed to backup failed L1 tx to store`, err);
249
+ });
208
250
  }
209
251
 
210
252
  public getRollupContract(): RollupContract {
@@ -386,19 +428,36 @@ export class SequencerPublisher {
386
428
  validRequests.sort((a, b) => compareActions(a.action, b.action));
387
429
 
388
430
  try {
431
+ // Capture context for failed tx backup before sending
432
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
433
+ const multicallData = encodeFunctionData({
434
+ abi: multicall3Abi,
435
+ functionName: 'aggregate3',
436
+ args: [
437
+ validRequests.map(r => ({
438
+ target: r.request.to!,
439
+ callData: r.request.data!,
440
+ allowFailure: true,
441
+ })),
442
+ ],
443
+ });
444
+ const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined;
445
+
446
+ const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber };
447
+
389
448
  this.log.debug('Forwarding transactions', {
390
449
  validRequests: validRequests.map(request => request.action),
391
450
  txConfig,
392
451
  });
393
- const result = await Multicall3.forward(
394
- validRequests.map(request => request.request),
395
- this.l1TxUtils,
396
- txConfig,
397
- blobConfig,
398
- this.rollupContract.address,
399
- this.log,
452
+ const result = await this.forwardWithPublisherRotation(validRequests, txConfig, blobConfig);
453
+ if (result === undefined) {
454
+ return undefined;
455
+ }
456
+ const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(
457
+ validRequests,
458
+ result,
459
+ txContext,
400
460
  );
401
- const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result);
402
461
  return { result, expiredActions, sentActions: validActions, successfulActions, failedActions };
403
462
  } catch (err) {
404
463
  const viemError = formatViemError(err);
@@ -416,13 +475,76 @@ export class SequencerPublisher {
416
475
  }
417
476
  }
418
477
 
478
+ /**
479
+ * Forwards transactions via Multicall3, rotating to the next available publisher if a send
480
+ * failure occurs (i.e. the tx never reached the chain).
481
+ * On-chain reverts and simulation errors are returned as-is without rotation.
482
+ */
483
+ private async forwardWithPublisherRotation(
484
+ validRequests: RequestWithExpiry[],
485
+ txConfig: RequestWithExpiry['gasConfig'],
486
+ blobConfig: L1BlobInputs | undefined,
487
+ ) {
488
+ const triedAddresses: EthAddress[] = [];
489
+ let currentPublisher = this.l1TxUtils;
490
+
491
+ while (true) {
492
+ triedAddresses.push(currentPublisher.getSenderAddress());
493
+ try {
494
+ const result = await Multicall3.forward(
495
+ validRequests.map(r => r.request),
496
+ currentPublisher,
497
+ txConfig,
498
+ blobConfig,
499
+ this.rollupContract.address,
500
+ this.log,
501
+ );
502
+ this.l1TxUtils = currentPublisher;
503
+ return result;
504
+ } catch (err) {
505
+ if (err instanceof TimeoutError) {
506
+ throw err;
507
+ }
508
+ const viemError = formatViemError(err);
509
+ if (!this.getNextPublisher) {
510
+ this.log.error('Failed to publish bundled transactions', viemError);
511
+ return undefined;
512
+ }
513
+ this.log.warn(
514
+ `Publisher ${currentPublisher.getSenderAddress()} failed to send, rotating to next publisher`,
515
+ viemError,
516
+ );
517
+ const nextPublisher = await this.getNextPublisher([...triedAddresses]);
518
+ if (!nextPublisher) {
519
+ this.log.error('All available publishers exhausted, failed to publish bundled transactions');
520
+ return undefined;
521
+ }
522
+ currentPublisher = nextPublisher;
523
+ }
524
+ }
525
+ }
526
+
419
527
  private callbackBundledTransactions(
420
528
  requests: RequestWithExpiry[],
421
- result?: { receipt: TransactionReceipt } | FormattedViemError,
529
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
530
+ txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint },
422
531
  ) {
423
532
  const actionsListStr = requests.map(r => r.action).join(', ');
424
533
  if (result instanceof FormattedViemError) {
425
534
  this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result);
535
+ this.backupFailedTx({
536
+ id: keccak256(txContext.multicallData),
537
+ failureType: 'send-error',
538
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
539
+ blobData: txContext.blobData,
540
+ l1BlockNumber: txContext.l1BlockNumber.toString(),
541
+ error: { message: result.message, name: result.name },
542
+ context: {
543
+ actions: requests.map(r => r.action),
544
+ requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
545
+ sender: this.getSenderAddress().toString(),
546
+ },
547
+ });
426
548
  return { failedActions: requests.map(r => r.action) };
427
549
  } else {
428
550
  this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests });
@@ -435,6 +557,30 @@ export class SequencerPublisher {
435
557
  failedActions.push(request.action);
436
558
  }
437
559
  }
560
+ // Single backup for the whole reverted tx
561
+ if (failedActions.length > 0 && result?.receipt?.status === 'reverted') {
562
+ this.backupFailedTx({
563
+ id: result.receipt.transactionHash,
564
+ failureType: 'revert',
565
+ request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData },
566
+ blobData: txContext.blobData,
567
+ l1BlockNumber: result.receipt.blockNumber.toString(),
568
+ receipt: {
569
+ transactionHash: result.receipt.transactionHash,
570
+ blockNumber: result.receipt.blockNumber.toString(),
571
+ gasUsed: (result.receipt.gasUsed ?? 0n).toString(),
572
+ status: 'reverted',
573
+ },
574
+ error: { message: result.errorMsg ?? 'Transaction reverted' },
575
+ context: {
576
+ actions: failedActions,
577
+ requests: requests
578
+ .filter(r => failedActions.includes(r.action))
579
+ .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })),
580
+ sender: this.getSenderAddress().toString(),
581
+ },
582
+ });
583
+ }
438
584
  return { successfulActions, failedActions };
439
585
  }
440
586
  }
@@ -546,6 +692,8 @@ export class SequencerPublisher {
546
692
  const request = this.buildInvalidateCheckpointRequest(validationResult);
547
693
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
548
694
 
695
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
696
+
549
697
  try {
550
698
  const { gasUsed } = await this.l1TxUtils.simulate(
551
699
  request,
@@ -597,6 +745,18 @@ export class SequencerPublisher {
597
745
 
598
746
  // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one.
599
747
  this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData);
748
+ this.backupFailedTx({
749
+ id: keccak256(request.data!),
750
+ failureType: 'simulation',
751
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
752
+ l1BlockNumber: l1BlockNumber.toString(),
753
+ error: { message: viemError.message, name: viemError.name },
754
+ context: {
755
+ actions: [`invalidate-${reason}`],
756
+ checkpointNumber,
757
+ sender: this.getSenderAddress().toString(),
758
+ },
759
+ });
600
760
  throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError });
601
761
  }
602
762
  }
@@ -744,11 +904,26 @@ export class SequencerPublisher {
744
904
  lastValidL2Slot: slotNumber,
745
905
  });
746
906
 
907
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
908
+
747
909
  try {
748
910
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
749
911
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
750
912
  } catch (err) {
751
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
913
+ const viemError = formatViemError(err);
914
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
915
+ this.backupFailedTx({
916
+ id: keccak256(request.data!),
917
+ failureType: 'simulation',
918
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
919
+ l1BlockNumber: l1BlockNumber.toString(),
920
+ error: { message: viemError.message, name: viemError.name },
921
+ context: {
922
+ actions: [action],
923
+ slot: slotNumber,
924
+ sender: this.getSenderAddress().toString(),
925
+ },
926
+ });
752
927
  // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself
753
928
  }
754
929
 
@@ -1044,6 +1219,8 @@ export class SequencerPublisher {
1044
1219
 
1045
1220
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1046
1221
 
1222
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1223
+
1047
1224
  let gasUsed: bigint;
1048
1225
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1049
1226
  try {
@@ -1053,6 +1230,19 @@ export class SequencerPublisher {
1053
1230
  const viemError = formatViemError(err, simulateAbi);
1054
1231
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1055
1232
 
1233
+ this.backupFailedTx({
1234
+ id: keccak256(request.data!),
1235
+ failureType: 'simulation',
1236
+ request: { to: request.to!, data: request.data!, value: request.value?.toString() },
1237
+ l1BlockNumber: l1BlockNumber.toString(),
1238
+ error: { message: viemError.message, name: viemError.name },
1239
+ context: {
1240
+ actions: [action],
1241
+ slot: slotNumber,
1242
+ sender: this.getSenderAddress().toString(),
1243
+ },
1244
+ });
1245
+
1056
1246
  return false;
1057
1247
  }
1058
1248
 
@@ -1136,9 +1326,27 @@ export class SequencerPublisher {
1136
1326
  kzg,
1137
1327
  },
1138
1328
  )
1139
- .catch(err => {
1140
- const { message, metaMessages } = formatViemError(err);
1141
- this.log.error(`Failed to validate blobs`, message, { metaMessages });
1329
+ .catch(async err => {
1330
+ const viemError = formatViemError(err);
1331
+ this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages });
1332
+ const validateBlobsData = encodeFunctionData({
1333
+ abi: RollupAbi,
1334
+ functionName: 'validateBlobs',
1335
+ args: [blobInput],
1336
+ });
1337
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1338
+ this.backupFailedTx({
1339
+ id: keccak256(validateBlobsData),
1340
+ failureType: 'simulation',
1341
+ request: { to: this.rollupContract.address as Hex, data: validateBlobsData },
1342
+ blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[],
1343
+ l1BlockNumber: l1BlockNumber.toString(),
1344
+ error: { message: viemError.message, name: viemError.name },
1345
+ context: {
1346
+ actions: ['validate-blobs'],
1347
+ sender: this.getSenderAddress().toString(),
1348
+ },
1349
+ });
1142
1350
  throw new Error('Failed to validate blobs');
1143
1351
  });
1144
1352
  }
@@ -1217,6 +1425,8 @@ export class SequencerPublisher {
1217
1425
  });
1218
1426
  }
1219
1427
 
1428
+ const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1429
+
1220
1430
  const simulationResult = await this.l1TxUtils
1221
1431
  .simulate(
1222
1432
  {
@@ -1250,6 +1460,18 @@ export class SequencerPublisher {
1250
1460
  };
1251
1461
  }
1252
1462
  this.log.error(`Failed to simulate propose tx`, viemError);
1463
+ this.backupFailedTx({
1464
+ id: keccak256(rollupData),
1465
+ failureType: 'simulation',
1466
+ request: { to: this.rollupContract.address, data: rollupData },
1467
+ l1BlockNumber: l1BlockNumber.toString(),
1468
+ error: { message: viemError.message, name: viemError.name },
1469
+ context: {
1470
+ actions: ['propose'],
1471
+ slot: Number(args[0].header.slotNumber),
1472
+ sender: this.getSenderAddress().toString(),
1473
+ },
1474
+ });
1253
1475
  throw err;
1254
1476
  });
1255
1477
 
@@ -9,6 +9,11 @@ import {
9
9
  SlotNumber,
10
10
  } from '@aztec/foundation/branded-types';
11
11
  import { randomInt } from '@aztec/foundation/crypto/random';
12
+ import {
13
+ flipSignature,
14
+ generateRecoverableSignature,
15
+ generateUnrecoverableSignature,
16
+ } from '@aztec/foundation/crypto/secp256k1-signer';
12
17
  import { Fr } from '@aztec/foundation/curves/bn254';
13
18
  import { EthAddress } from '@aztec/foundation/eth-address';
14
19
  import { Signature } from '@aztec/foundation/eth-signature';
@@ -38,7 +43,7 @@ import {
38
43
  } from '@aztec/stdlib/interfaces/server';
39
44
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
40
45
  import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
41
- import { orderAttestations } from '@aztec/stdlib/p2p';
46
+ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
42
47
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
43
48
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
44
49
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
@@ -743,11 +748,28 @@ export class CheckpointProposalJob implements Traceable {
743
748
 
744
749
  collectedAttestationsCount = attestations.length;
745
750
 
751
+ // Trim attestations to minimum required to save L1 calldata gas
752
+ const localAddresses = this.validatorClient.getValidatorAddresses();
753
+ const trimmed = trimAttestations(
754
+ attestations,
755
+ numberOfRequiredAttestations,
756
+ this.attestorAddress,
757
+ localAddresses,
758
+ );
759
+ if (trimmed.length < attestations.length) {
760
+ this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
761
+ }
762
+
746
763
  // Rollup contract requires that the signatures are provided in the order of the committee
747
- const sorted = orderAttestations(attestations, committee);
764
+ const sorted = orderAttestations(trimmed, committee);
748
765
 
749
766
  // Manipulate the attestations if we've been configured to do so
750
- if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
767
+ if (
768
+ this.config.injectFakeAttestation ||
769
+ this.config.injectHighSValueAttestation ||
770
+ this.config.injectUnrecoverableSignatureAttestation ||
771
+ this.config.shuffleAttestationOrdering
772
+ ) {
751
773
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
752
774
  }
753
775
 
@@ -776,7 +798,11 @@ export class CheckpointProposalJob implements Traceable {
776
798
  this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
777
799
  );
778
800
 
779
- if (this.config.injectFakeAttestation) {
801
+ if (
802
+ this.config.injectFakeAttestation ||
803
+ this.config.injectHighSValueAttestation ||
804
+ this.config.injectUnrecoverableSignatureAttestation
805
+ ) {
780
806
  // Find non-empty attestations that are not from the proposer
781
807
  const nonProposerIndices: number[] = [];
782
808
  for (let i = 0; i < attestations.length; i++) {
@@ -786,8 +812,20 @@ export class CheckpointProposalJob implements Traceable {
786
812
  }
787
813
  if (nonProposerIndices.length > 0) {
788
814
  const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
789
- this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
790
- unfreeze(attestations[targetIndex]).signature = Signature.random();
815
+ if (this.config.injectHighSValueAttestation) {
816
+ this.log.warn(
817
+ `Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
818
+ );
819
+ unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
820
+ } else if (this.config.injectUnrecoverableSignatureAttestation) {
821
+ this.log.warn(
822
+ `Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
823
+ );
824
+ unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
825
+ } else {
826
+ this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
827
+ unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
828
+ }
791
829
  }
792
830
  return new CommitteeAttestationsAndSigners(attestations);
793
831
  }
@@ -796,11 +834,20 @@ export class CheckpointProposalJob implements Traceable {
796
834
  this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
797
835
 
798
836
  const shuffled = [...attestations];
799
- const [i, j] = [(proposerIndex + 1) % shuffled.length, (proposerIndex + 2) % shuffled.length];
800
- const valueI = shuffled[i];
801
- const valueJ = shuffled[j];
802
- shuffled[i] = valueJ;
803
- shuffled[j] = valueI;
837
+
838
+ // Find two non-proposer positions that both have non-empty signatures to swap.
839
+ // This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
840
+ // signers array stays correctly aligned with L1's committee reconstruction.
841
+ const swappable: number[] = [];
842
+ for (let k = 0; k < shuffled.length; k++) {
843
+ if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
844
+ swappable.push(k);
845
+ }
846
+ }
847
+ if (swappable.length >= 2) {
848
+ const [i, j] = [swappable[0], swappable[1]];
849
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
850
+ }
804
851
 
805
852
  const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
806
853
  return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
@@ -110,7 +110,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
110
110
  /** Updates sequencer config by the defined values and updates the timetable */
111
111
  public updateConfig(config: Partial<SequencerConfig>) {
112
112
  const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
113
- this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
113
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
114
114
  this.config = merge(this.config, filteredConfig);
115
115
  this.timetable = new SequencerTimetable(
116
116
  {
@@ -422,6 +422,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
422
422
  );
423
423
  }
424
424
 
425
+ /**
426
+ * Returns the current sequencer state.
427
+ */
428
+ public getState(): SequencerState {
429
+ return this.state;
430
+ }
431
+
425
432
  /**
426
433
  * Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
427
434
  * @param proposedState - The new state to transition to.
@@ -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,