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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dest/client/sequencer-client.d.ts +1 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +0 -4
  4. package/dest/global_variable_builder/global_builder.d.ts +3 -3
  5. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  6. package/dest/global_variable_builder/global_builder.js +7 -4
  7. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
  8. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  9. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  10. package/dest/publisher/sequencer-publisher.d.ts +52 -31
  11. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  12. package/dest/publisher/sequencer-publisher.js +106 -87
  13. package/dest/sequencer/checkpoint_proposal_job.d.ts +31 -10
  14. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  15. package/dest/sequencer/checkpoint_proposal_job.js +179 -108
  16. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  17. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  18. package/dest/sequencer/checkpoint_voter.js +2 -5
  19. package/dest/sequencer/sequencer.d.ts +14 -4
  20. package/dest/sequencer/sequencer.d.ts.map +1 -1
  21. package/dest/sequencer/sequencer.js +67 -18
  22. package/dest/sequencer/timetable.d.ts +4 -1
  23. package/dest/sequencer/timetable.d.ts.map +1 -1
  24. package/dest/sequencer/timetable.js +15 -5
  25. package/package.json +27 -27
  26. package/src/client/sequencer-client.ts +0 -7
  27. package/src/global_variable_builder/global_builder.ts +15 -3
  28. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  29. package/src/publisher/sequencer-publisher.ts +174 -124
  30. package/src/sequencer/README.md +81 -12
  31. package/src/sequencer/checkpoint_proposal_job.ts +215 -117
  32. package/src/sequencer/checkpoint_voter.ts +1 -12
  33. package/src/sequencer/sequencer.ts +97 -20
  34. package/src/sequencer/timetable.ts +19 -8
@@ -3,14 +3,14 @@ import { Blob, getBlobsPerL1Block, getPrefixedEthBlobCommitments } from '@aztec/
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
- type EmpireSlashingProposerContract,
7
6
  FeeAssetPriceOracle,
7
+ type FeeHeader,
8
8
  type GovernanceProposerContract,
9
9
  type IEmpireBase,
10
10
  MULTI_CALL_3_ADDRESS,
11
11
  Multicall3,
12
12
  RollupContract,
13
- type TallySlashingProposerContract,
13
+ type SlashingProposerContract,
14
14
  type ViemCommitteeAttestations,
15
15
  type ViemHeader,
16
16
  } from '@aztec/ethereum/contracts';
@@ -36,14 +36,14 @@ import { EthAddress } from '@aztec/foundation/eth-address';
36
36
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
37
37
  import { type Logger, createLogger } from '@aztec/foundation/log';
38
38
  import { makeBackoff, retry } from '@aztec/foundation/retry';
39
+ import { InterruptibleSleep } from '@aztec/foundation/sleep';
39
40
  import { bufferToHex } from '@aztec/foundation/string';
40
- import { DateProvider, Timer } from '@aztec/foundation/timer';
41
+ import { type DateProvider, Timer } from '@aztec/foundation/timer';
41
42
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
42
43
  import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slasher';
43
44
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
44
45
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
45
- import { getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
46
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
46
+ import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
47
47
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
48
48
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
49
49
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
@@ -63,6 +63,20 @@ import type { SequencerPublisherConfig } from './config.js';
63
63
  import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js';
64
64
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
65
65
 
66
+ /** Result of a sendRequests call, returned by both sendRequests() and sendRequestsAt(). */
67
+ export type SendRequestsResult = {
68
+ /** The L1 transaction receipt or error from the bundled multicall. */
69
+ result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError;
70
+ /** Actions that expired (past their deadline) before the request was sent. */
71
+ expiredActions: Action[];
72
+ /** Actions that were included in the sent L1 transaction. */
73
+ sentActions: Action[];
74
+ /** Actions whose L1 simulation succeeded (subset of sentActions). */
75
+ successfulActions: Action[];
76
+ /** Actions whose L1 simulation failed (subset of sentActions). */
77
+ failedActions: Action[];
78
+ };
79
+
66
80
  /** Arguments to the process method of the rollup contract */
67
81
  type L1ProcessArgs = {
68
82
  /** The L2 block header. */
@@ -84,16 +98,13 @@ export const Actions = [
84
98
  'invalidate-by-insufficient-attestations',
85
99
  'propose',
86
100
  'governance-signal',
87
- 'empire-slashing-signal',
88
- 'create-empire-payload',
89
- 'execute-empire-payload',
90
101
  'vote-offenses',
91
102
  'execute-slash',
92
103
  ] as const;
93
104
 
94
105
  export type Action = (typeof Actions)[number];
95
106
 
96
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
97
108
 
98
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
99
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -104,6 +115,8 @@ export type InvalidateCheckpointRequest = {
104
115
  gasUsed: bigint;
105
116
  checkpointNumber: CheckpointNumber;
106
117
  forcePendingCheckpointNumber: CheckpointNumber;
118
+ /** Archive at the rollback target checkpoint (checkpoint N-1). */
119
+ lastArchive: Fr;
107
120
  };
108
121
 
109
122
  interface RequestWithExpiry {
@@ -112,6 +125,8 @@ interface RequestWithExpiry {
112
125
  lastValidL2Slot: SlotNumber;
113
126
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
114
127
  blobConfig?: L1BlobInputs;
128
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
129
+ preCheck?: () => Promise<void>;
115
130
  checkSuccess: (
116
131
  request: L1TxRequest,
117
132
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -135,7 +150,9 @@ export class SequencerPublisher {
135
150
  protected log: Logger;
136
151
  protected ethereumSlotDuration: bigint;
137
152
  protected aztecSlotDuration: bigint;
138
- private dateProvider: DateProvider;
153
+
154
+ /** Date provider for wall-clock time. */
155
+ private readonly dateProvider: DateProvider;
139
156
 
140
157
  private blobClient: BlobClientInterface;
141
158
 
@@ -151,6 +168,9 @@ export class SequencerPublisher {
151
168
  /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
152
169
  private feeAssetPriceOracle: FeeAssetPriceOracle;
153
170
 
171
+ /** Interruptible sleep used by sendRequestsAt to wait until a target timestamp. */
172
+ private readonly interruptibleSleep = new InterruptibleSleep();
173
+
154
174
  // A CALL to a cold address is 2700 gas
155
175
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
156
176
 
@@ -160,8 +180,7 @@ export class SequencerPublisher {
160
180
  public l1TxUtils: L1TxUtils;
161
181
  public rollupContract: RollupContract;
162
182
  public govProposerContract: GovernanceProposerContract;
163
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
164
- public slashFactoryContract: SlashFactoryContract;
183
+ public slashingProposerContract: SlashingProposerContract | undefined;
165
184
 
166
185
  public readonly tracer: Tracer;
167
186
 
@@ -175,9 +194,8 @@ export class SequencerPublisher {
175
194
  blobClient: BlobClientInterface;
176
195
  l1TxUtils: L1TxUtils;
177
196
  rollupContract: RollupContract;
178
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
197
+ slashingProposerContract: SlashingProposerContract | undefined;
179
198
  governanceProposerContract: GovernanceProposerContract;
180
- slashFactoryContract: SlashFactoryContract;
181
199
  epochCache: EpochCache;
182
200
  dateProvider: DateProvider;
183
201
  metrics: SequencerPublisherMetrics;
@@ -194,6 +212,7 @@ export class SequencerPublisher {
194
212
  this.lastActions = deps.lastActions;
195
213
 
196
214
  this.blobClient = deps.blobClient;
215
+ this.dateProvider = deps.dateProvider;
197
216
 
198
217
  const telemetry = deps.telemetry ?? getTelemetryClient();
199
218
  this.metrics = deps.metrics ?? new SequencerPublisherMetrics(telemetry, 'SequencerPublisher');
@@ -211,8 +230,6 @@ export class SequencerPublisher {
211
230
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
212
231
  this.slashingProposerContract = newSlashingProposer;
213
232
  });
214
- this.slashFactoryContract = deps.slashFactoryContract;
215
-
216
233
  // Initialize L1 fee analyzer for fisherman mode
217
234
  if (config.fishermanMode) {
218
235
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -369,9 +386,10 @@ export class SequencerPublisher {
369
386
  * - undefined if no valid requests are found OR the tx failed to send.
370
387
  */
371
388
  @trackSpan('SequencerPublisher.sendRequests')
372
- public async sendRequests() {
389
+ public async sendRequests(): Promise<SendRequestsResult | undefined> {
373
390
  const requestsToProcess = [...this.requests];
374
391
  this.requests = [];
392
+
375
393
  if (this.interrupted || requestsToProcess.length === 0) {
376
394
  return undefined;
377
395
  }
@@ -530,6 +548,45 @@ export class SequencerPublisher {
530
548
  }
531
549
  }
532
550
 
551
+ /*
552
+ * Schedules sending all enqueued requests at (or after) the given timestamp.
553
+ * Uses InterruptibleSleep so it can be cancelled via interrupt().
554
+ * Returns the promise for the L1 response (caller should NOT await this in the work loop).
555
+ */
556
+ public async sendRequestsAt(submitAfter: Date): Promise<SendRequestsResult | undefined> {
557
+ const ms = submitAfter.getTime() - this.dateProvider.now();
558
+ if (ms > 0) {
559
+ this.log.debug(`Sleeping ${ms}ms before sending requests`, { submitAfter });
560
+ await this.interruptibleSleep.sleep(ms);
561
+ }
562
+ if (this.interrupted) {
563
+ return undefined;
564
+ }
565
+
566
+ // Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
567
+ const validRequests: RequestWithExpiry[] = [];
568
+ for (const request of this.requests) {
569
+ if (!request.preCheck) {
570
+ validRequests.push(request);
571
+ continue;
572
+ }
573
+
574
+ try {
575
+ await request.preCheck();
576
+ validRequests.push(request);
577
+ } catch (err) {
578
+ this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
579
+ }
580
+ }
581
+
582
+ this.requests = validRequests;
583
+ if (this.requests.length === 0) {
584
+ return undefined;
585
+ }
586
+
587
+ return this.sendRequests();
588
+ }
589
+
533
590
  private callbackBundledTransactions(
534
591
  requests: RequestWithExpiry[],
535
592
  result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined,
@@ -608,7 +665,11 @@ export class SequencerPublisher {
608
665
  public canProposeAt(
609
666
  tipArchive: Fr,
610
667
  msgSender: EthAddress,
611
- opts: { forcePendingCheckpointNumber?: CheckpointNumber; pipelined?: boolean } = {},
668
+ opts: {
669
+ forcePendingCheckpointNumber?: CheckpointNumber;
670
+ forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
671
+ pipelined?: boolean;
672
+ } = {},
612
673
  ) {
613
674
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
614
675
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
@@ -620,6 +681,7 @@ export class SequencerPublisher {
620
681
  return this.rollupContract
621
682
  .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
622
683
  forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
684
+ forceArchive: opts.forceArchive,
623
685
  })
624
686
  .catch(err => {
625
687
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
@@ -656,7 +718,7 @@ export class SequencerPublisher {
656
718
  flags,
657
719
  ] as const;
658
720
 
659
- const ts = this.getNextL1SlotTimestamp();
721
+ const ts = this.getSimulationTimestamp(header.slotNumber);
660
722
  const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
661
723
  opts?.forcePendingCheckpointNumber,
662
724
  );
@@ -679,7 +741,7 @@ export class SequencerPublisher {
679
741
  data: encodeFunctionData({ abi: RollupAbi, functionName: 'validateHeaderWithAttestations', args }),
680
742
  from: MULTI_CALL_3_ADDRESS,
681
743
  },
682
- { time: ts + 1n },
744
+ { time: ts },
683
745
  stateOverrides,
684
746
  );
685
747
  this.log.debug(`Simulated validateHeader`);
@@ -732,6 +794,7 @@ export class SequencerPublisher {
732
794
  gasUsed,
733
795
  checkpointNumber,
734
796
  forcePendingCheckpointNumber: CheckpointNumber(checkpointNumber - 1),
797
+ lastArchive: validationResult.checkpoint.lastArchive,
735
798
  reason,
736
799
  };
737
800
  } catch (err) {
@@ -744,8 +807,8 @@ export class SequencerPublisher {
744
807
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
745
808
  { ...logData, request, error: viemError.message },
746
809
  );
747
- const latestPendingCheckpointNumber = await this.rollupContract.getCheckpointNumber();
748
- if (latestPendingCheckpointNumber < checkpointNumber) {
810
+ const latestProposedCheckpointNumber = await this.rollupContract.getCheckpointNumber();
811
+ if (latestProposedCheckpointNumber < checkpointNumber) {
749
812
  this.log.verbose(`Checkpoint ${checkpointNumber} has already been invalidated`, { ...logData });
750
813
  return undefined;
751
814
  } else {
@@ -819,11 +882,11 @@ export class SequencerPublisher {
819
882
  checkpoint: Checkpoint,
820
883
  attestationsAndSigners: CommitteeAttestationsAndSigners,
821
884
  attestationsAndSignersSignature: Signature,
822
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
823
- ): Promise<bigint> {
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;
885
+ options: {
886
+ forcePendingCheckpointNumber?: CheckpointNumber;
887
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
888
+ },
889
+ ): Promise<void> {
827
890
  const blobFields = checkpoint.toBlobFields();
828
891
  const blobs = await getBlobsPerL1Block(blobFields);
829
892
  const blobInput = getPrefixedEthBlobCommitments(blobs);
@@ -842,13 +905,11 @@ export class SequencerPublisher {
842
905
  blobInput,
843
906
  ] as const;
844
907
 
845
- await this.simulateProposeTx(args, ts, options);
846
- return ts;
908
+ await this.simulateProposeTx(args, options);
847
909
  }
848
910
 
849
911
  private async enqueueCastSignalHelper(
850
912
  slotNumber: SlotNumber,
851
- timestamp: bigint,
852
913
  signalType: GovernanceSignalAction,
853
914
  payload: EthAddress,
854
915
  base: IEmpireBase,
@@ -927,13 +988,17 @@ export class SequencerPublisher {
927
988
  });
928
989
 
929
990
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
991
+ const timestamp = this.getSimulationTimestamp(slotNumber);
930
992
 
931
993
  try {
932
994
  await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
933
995
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
934
996
  } catch (err) {
935
997
  const viemError = formatViemError(err);
936
- this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError);
998
+ this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError, {
999
+ simulationTimestamp: timestamp,
1000
+ l1BlockNumber,
1001
+ });
937
1002
  this.backupFailedTx({
938
1003
  id: keccak256(request.data!),
939
1004
  failureType: 'simulation',
@@ -996,19 +1061,16 @@ export class SequencerPublisher {
996
1061
  /**
997
1062
  * Enqueues a governance castSignal transaction to cast a signal for a given slot number.
998
1063
  * @param slotNumber - The slot number to cast a signal for.
999
- * @param timestamp - The timestamp of the slot to cast a signal for.
1000
1064
  * @returns True if the signal was successfully enqueued, false otherwise.
1001
1065
  */
1002
1066
  public enqueueGovernanceCastSignal(
1003
1067
  governancePayload: EthAddress,
1004
1068
  slotNumber: SlotNumber,
1005
- timestamp: bigint,
1006
1069
  signerAddress: EthAddress,
1007
1070
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1008
1071
  ): Promise<boolean> {
1009
1072
  return this.enqueueCastSignalHelper(
1010
1073
  slotNumber,
1011
- timestamp,
1012
1074
  'governance-signal',
1013
1075
  governancePayload,
1014
1076
  this.govProposerContract,
@@ -1021,7 +1083,6 @@ export class SequencerPublisher {
1021
1083
  public async enqueueSlashingActions(
1022
1084
  actions: ProposerSlashAction[],
1023
1085
  slotNumber: SlotNumber,
1024
- timestamp: bigint,
1025
1086
  signerAddress: EthAddress,
1026
1087
  signer: (msg: TypedDataDefinition) => Promise<`0x${string}`>,
1027
1088
  ): Promise<boolean> {
@@ -1032,58 +1093,6 @@ export class SequencerPublisher {
1032
1093
 
1033
1094
  for (const action of actions) {
1034
1095
  switch (action.type) {
1035
- case 'vote-empire-payload': {
1036
- if (this.slashingProposerContract?.type !== 'empire') {
1037
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
1038
- break;
1039
- }
1040
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
1041
- signerAddress,
1042
- });
1043
- await this.enqueueCastSignalHelper(
1044
- slotNumber,
1045
- timestamp,
1046
- 'empire-slashing-signal',
1047
- action.payload,
1048
- this.slashingProposerContract,
1049
- signerAddress,
1050
- signer,
1051
- );
1052
- break;
1053
- }
1054
-
1055
- case 'create-empire-payload': {
1056
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
1057
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
1058
- await this.simulateAndEnqueueRequest(
1059
- 'create-empire-payload',
1060
- request,
1061
- (receipt: TransactionReceipt) =>
1062
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
1063
- slotNumber,
1064
- timestamp,
1065
- );
1066
- break;
1067
- }
1068
-
1069
- case 'execute-empire-payload': {
1070
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
1071
- if (this.slashingProposerContract?.type !== 'empire') {
1072
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
1073
- return false;
1074
- }
1075
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
1076
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1077
- await this.simulateAndEnqueueRequest(
1078
- 'execute-empire-payload',
1079
- request,
1080
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
1081
- slotNumber,
1082
- timestamp,
1083
- );
1084
- break;
1085
- }
1086
-
1087
1096
  case 'vote-offenses': {
1088
1097
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
1089
1098
  slotNumber,
@@ -1091,19 +1100,17 @@ export class SequencerPublisher {
1091
1100
  votesCount: action.votes.length,
1092
1101
  signerAddress,
1093
1102
  });
1094
- if (this.slashingProposerContract?.type !== 'tally') {
1095
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1103
+ if (!this.slashingProposerContract) {
1104
+ this.log.error('No slashing proposer contract available');
1096
1105
  return false;
1097
1106
  }
1098
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1099
1107
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1100
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1108
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
1101
1109
  await this.simulateAndEnqueueRequest(
1102
1110
  'vote-offenses',
1103
1111
  request,
1104
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1112
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
1105
1113
  slotNumber,
1106
- timestamp,
1107
1114
  );
1108
1115
  break;
1109
1116
  }
@@ -1114,18 +1121,20 @@ export class SequencerPublisher {
1114
1121
  round: action.round,
1115
1122
  signerAddress,
1116
1123
  });
1117
- if (this.slashingProposerContract?.type !== 'tally') {
1118
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1124
+ if (!this.slashingProposerContract) {
1125
+ this.log.error('No slashing proposer contract available');
1119
1126
  return false;
1120
1127
  }
1121
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1122
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1128
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1129
+ action.round,
1130
+ action.committees,
1131
+ );
1123
1132
  await this.simulateAndEnqueueRequest(
1124
1133
  'execute-slash',
1125
- request,
1126
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1134
+ executeRequest,
1135
+ (receipt: TransactionReceipt) =>
1136
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
1127
1137
  slotNumber,
1128
- timestamp,
1129
1138
  );
1130
1139
  break;
1131
1140
  }
@@ -1145,7 +1154,11 @@ export class SequencerPublisher {
1145
1154
  checkpoint: Checkpoint,
1146
1155
  attestationsAndSigners: CommitteeAttestationsAndSigners,
1147
1156
  attestationsAndSignersSignature: Signature,
1148
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1157
+ opts: {
1158
+ txTimeoutAt?: Date;
1159
+ forcePendingCheckpointNumber?: CheckpointNumber;
1160
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1161
+ } = {},
1149
1162
  ): Promise<void> {
1150
1163
  const checkpointHeader = checkpoint.header;
1151
1164
 
@@ -1161,15 +1174,13 @@ export class SequencerPublisher {
1161
1174
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
1162
1175
  };
1163
1176
 
1164
- let ts: bigint;
1165
-
1166
1177
  try {
1167
1178
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1168
1179
  // This means that we can avoid the simulation issues in later checks.
1169
1180
  // By simulation issue, I mean the fact that the block.timestamp is equal to the last block, not the next, which
1170
1181
  // make time consistency checks break.
1171
1182
  // TODO(palla): Check whether we're validating twice, once here and once within addProposeTx, since we call simulateProposeTx in both places.
1172
- ts = await this.validateCheckpointForSubmission(
1183
+ await this.validateCheckpointForSubmission(
1173
1184
  checkpoint,
1174
1185
  attestationsAndSigners,
1175
1186
  attestationsAndSignersSignature,
@@ -1184,8 +1195,26 @@ export class SequencerPublisher {
1184
1195
  throw err;
1185
1196
  }
1186
1197
 
1198
+ // Build a pre-check callback that re-validates the checkpoint before L1 submission.
1199
+ // During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
1200
+ let preCheck = undefined;
1201
+ if (this.epochCache.isProposerPipeliningEnabled()) {
1202
+ preCheck = async () => {
1203
+ this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
1204
+ await this.validateCheckpointForSubmission(
1205
+ checkpoint,
1206
+ attestationsAndSigners,
1207
+ attestationsAndSignersSignature,
1208
+ {
1209
+ // Forcing pending checkpoint number is included its required if an invalidation request is included
1210
+ forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1211
+ },
1212
+ );
1213
+ };
1214
+ }
1215
+
1187
1216
  this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1188
- await this.addProposeTx(checkpoint, proposeTxArgs, opts, ts);
1217
+ await this.addProposeTx(checkpoint, proposeTxArgs, opts, preCheck);
1189
1218
  }
1190
1219
 
1191
1220
  public enqueueInvalidateCheckpoint(
@@ -1228,8 +1257,8 @@ export class SequencerPublisher {
1228
1257
  request: L1TxRequest,
1229
1258
  checkSuccess: (receipt: TransactionReceipt) => boolean | undefined,
1230
1259
  slotNumber: SlotNumber,
1231
- timestamp: bigint,
1232
1260
  ) {
1261
+ const timestamp = this.getSimulationTimestamp(slotNumber);
1233
1262
  const logData = { slotNumber, timestamp, gasLimit: undefined as bigint | undefined };
1234
1263
  if (this.lastActions[action] && this.lastActions[action] === slotNumber) {
1235
1264
  this.log.debug(`Skipping duplicate action ${action} for slot ${slotNumber}`);
@@ -1245,8 +1274,9 @@ export class SequencerPublisher {
1245
1274
 
1246
1275
  let gasUsed: bigint;
1247
1276
  const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1277
+
1248
1278
  try {
1249
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1279
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi));
1250
1280
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1251
1281
  } catch (err) {
1252
1282
  const viemError = formatViemError(err, simulateAbi);
@@ -1304,6 +1334,7 @@ export class SequencerPublisher {
1304
1334
  */
1305
1335
  public interrupt() {
1306
1336
  this.interrupted = true;
1337
+ this.interruptibleSleep.interrupt();
1307
1338
  this.l1TxUtils.interrupt();
1308
1339
  }
1309
1340
 
@@ -1315,7 +1346,6 @@ export class SequencerPublisher {
1315
1346
 
1316
1347
  private async prepareProposeTx(
1317
1348
  encodedData: L1ProcessArgs,
1318
- timestamp: bigint,
1319
1349
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
1320
1350
  ) {
1321
1351
  const kzg = Blob.getViemKzgInstance();
@@ -1388,7 +1418,7 @@ export class SequencerPublisher {
1388
1418
  blobInput,
1389
1419
  ] as const;
1390
1420
 
1391
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, timestamp, options);
1421
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1392
1422
 
1393
1423
  return { args, blobEvaluationGas, rollupData, simulationResult };
1394
1424
  }
@@ -1396,7 +1426,6 @@ export class SequencerPublisher {
1396
1426
  /**
1397
1427
  * Simulates the propose tx with eth_simulateV1
1398
1428
  * @param args - The propose tx args
1399
- * @param timestamp - The timestamp to simulate proposal at
1400
1429
  * @returns The simulation result
1401
1430
  */
1402
1431
  private async simulateProposeTx(
@@ -1413,8 +1442,10 @@ export class SequencerPublisher {
1413
1442
  ViemSignature,
1414
1443
  `0x${string}`,
1415
1444
  ],
1416
- timestamp: bigint,
1417
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1445
+ options: {
1446
+ forcePendingCheckpointNumber?: CheckpointNumber;
1447
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1448
+ },
1418
1449
  ) {
1419
1450
  const rollupData = encodeFunctionData({
1420
1451
  abi: RollupAbi,
@@ -1422,13 +1453,23 @@ export class SequencerPublisher {
1422
1453
  args,
1423
1454
  });
1424
1455
 
1425
- // override the pending checkpoint number if requested
1456
+ // override the proposed checkpoint number if requested
1426
1457
  const forcePendingCheckpointNumberStateDiff = (
1427
1458
  options.forcePendingCheckpointNumber !== undefined
1428
1459
  ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1429
1460
  : []
1430
1461
  ).flatMap(override => override.stateDiff ?? []);
1431
1462
 
1463
+ // override the fee header for a specific checkpoint number if requested (used when pipelining)
1464
+ const forceProposedFeeHeaderStateDiff = (
1465
+ options.forceProposedFeeHeader !== undefined
1466
+ ? await this.rollupContract.makeFeeHeaderOverride(
1467
+ options.forceProposedFeeHeader.checkpointNumber,
1468
+ options.forceProposedFeeHeader.feeHeader,
1469
+ )
1470
+ : []
1471
+ ).flatMap(override => override.stateDiff ?? []);
1472
+
1432
1473
  const stateOverrides: StateOverride = [
1433
1474
  {
1434
1475
  address: this.rollupContract.address,
@@ -1436,6 +1477,7 @@ export class SequencerPublisher {
1436
1477
  stateDiff: [
1437
1478
  { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1438
1479
  ...forcePendingCheckpointNumberStateDiff,
1480
+ ...forceProposedFeeHeaderStateDiff,
1439
1481
  ],
1440
1482
  },
1441
1483
  ];
@@ -1448,6 +1490,7 @@ export class SequencerPublisher {
1448
1490
  }
1449
1491
 
1450
1492
  const l1BlockNumber = await this.l1TxUtils.getBlockNumber();
1493
+ const simTs = this.getSimulationTimestamp(SlotNumber.fromBigInt(args[0].header.slotNumber));
1451
1494
 
1452
1495
  const simulationResult = await this.l1TxUtils
1453
1496
  .simulate(
@@ -1458,8 +1501,7 @@ export class SequencerPublisher {
1458
1501
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1459
1502
  },
1460
1503
  {
1461
- // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1462
- time: timestamp + 1n,
1504
+ time: simTs,
1463
1505
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1464
1506
  gasLimit: MAX_L1_TX_LIMIT * 2n,
1465
1507
  },
@@ -1481,7 +1523,7 @@ export class SequencerPublisher {
1481
1523
  logs: [],
1482
1524
  };
1483
1525
  }
1484
- this.log.error(`Failed to simulate propose tx`, viemError);
1526
+ this.log.error(`Failed to simulate propose tx`, viemError, { simulationTimestamp: simTs });
1485
1527
  this.backupFailedTx({
1486
1528
  id: keccak256(rollupData),
1487
1529
  failureType: 'simulation',
@@ -1503,17 +1545,17 @@ export class SequencerPublisher {
1503
1545
  private async addProposeTx(
1504
1546
  checkpoint: Checkpoint,
1505
1547
  encodedData: L1ProcessArgs,
1506
- opts: { txTimeoutAt?: Date; forcePendingCheckpointNumber?: CheckpointNumber } = {},
1507
- timestamp: bigint,
1548
+ opts: {
1549
+ txTimeoutAt?: Date;
1550
+ forcePendingCheckpointNumber?: CheckpointNumber;
1551
+ forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1552
+ } = {},
1553
+ preCheck?: () => Promise<void>,
1508
1554
  ): Promise<void> {
1509
1555
  const slot = checkpoint.header.slotNumber;
1510
1556
  const timer = new Timer();
1511
1557
  const kzg = Blob.getViemKzgInstance();
1512
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1513
- encodedData,
1514
- timestamp,
1515
- opts,
1516
- );
1558
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1517
1559
  const startBlock = await this.l1TxUtils.getBlockNumber();
1518
1560
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1519
1561
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1537,6 +1579,7 @@ export class SequencerPublisher {
1537
1579
  },
1538
1580
  lastValidL2Slot: checkpoint.header.slotNumber,
1539
1581
  gasConfig: { ...opts, gasLimit },
1582
+ preCheck,
1540
1583
  blobConfig: {
1541
1584
  blobs: encodedData.blobs.map(b => b.data),
1542
1585
  kzg,
@@ -1590,7 +1633,14 @@ export class SequencerPublisher {
1590
1633
  });
1591
1634
  }
1592
1635
 
1593
- /** Returns the timestamp to use when simulating L1 proposal calls */
1636
+ /** Returns the timestamp of the last L1 slot within a given L2 slot. Used as the simulation timestamp
1637
+ * for eth_simulateV1 calls, since it's guaranteed to be greater than any L1 block produced during the slot. */
1638
+ private getSimulationTimestamp(slot: SlotNumber): bigint {
1639
+ const l1Constants = this.epochCache.getL1Constants();
1640
+ return getLastL1SlotTimestampForL2Slot(slot, l1Constants);
1641
+ }
1642
+
1643
+ /** Returns the timestamp of the next L1 slot boundary after now. */
1594
1644
  private getNextL1SlotTimestamp(): bigint {
1595
1645
  const l1Constants = this.epochCache.getL1Constants();
1596
1646
  return getNextL1SlotTimestamp(this.dateProvider.nowInSeconds(), l1Constants);