@aztec/sequencer-client 0.0.1-commit.f5d02921e → 0.0.1-commit.f7ea82942

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 (69) hide show
  1. package/dest/client/sequencer-client.d.ts +3 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +3 -4
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +8 -11
  7. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  9. package/dest/global_variable_builder/fee_predictor.js +128 -0
  10. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  12. package/dest/global_variable_builder/fee_provider.js +58 -0
  13. package/dest/global_variable_builder/global_builder.d.ts +4 -9
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +3 -41
  16. package/dest/global_variable_builder/index.d.ts +3 -1
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/publisher/config.d.ts +1 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +2 -2
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
  25. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  27. package/dest/publisher/sequencer-publisher.d.ts +16 -35
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +60 -92
  30. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  31. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  32. package/dest/sequencer/chain_state_overrides.js +39 -0
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts +27 -22
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +294 -155
  36. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  37. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  38. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  39. package/dest/sequencer/events.d.ts +6 -1
  40. package/dest/sequencer/events.d.ts.map +1 -1
  41. package/dest/sequencer/metrics.d.ts +7 -10
  42. package/dest/sequencer/metrics.d.ts.map +1 -1
  43. package/dest/sequencer/metrics.js +27 -20
  44. package/dest/sequencer/sequencer.d.ts +5 -2
  45. package/dest/sequencer/sequencer.d.ts.map +1 -1
  46. package/dest/sequencer/sequencer.js +31 -23
  47. package/dest/sequencer/timetable.d.ts +14 -1
  48. package/dest/sequencer/timetable.d.ts.map +1 -1
  49. package/dest/sequencer/timetable.js +45 -36
  50. package/package.json +27 -27
  51. package/src/client/sequencer-client.ts +5 -7
  52. package/src/config.ts +8 -11
  53. package/src/global_variable_builder/README.md +44 -0
  54. package/src/global_variable_builder/fee_predictor.ts +172 -0
  55. package/src/global_variable_builder/fee_provider.ts +75 -0
  56. package/src/global_variable_builder/global_builder.ts +7 -54
  57. package/src/global_variable_builder/index.ts +2 -0
  58. package/src/publisher/config.ts +6 -4
  59. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  60. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  61. package/src/publisher/sequencer-publisher.ts +106 -152
  62. package/src/sequencer/README.md +83 -13
  63. package/src/sequencer/chain_state_overrides.ts +87 -0
  64. package/src/sequencer/checkpoint_proposal_job.ts +365 -205
  65. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  66. package/src/sequencer/events.ts +5 -0
  67. package/src/sequencer/metrics.ts +35 -25
  68. package/src/sequencer/sequencer.ts +44 -26
  69. package/src/sequencer/timetable.ts +57 -45
@@ -3,17 +3,18 @@ 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,
8
- type FeeHeader,
9
7
  type GovernanceProposerContract,
10
8
  type IEmpireBase,
11
9
  MULTI_CALL_3_ADDRESS,
12
10
  Multicall3,
13
11
  RollupContract,
14
- type TallySlashingProposerContract,
12
+ SimulationOverridesBuilder,
13
+ type SimulationOverridesPlan,
14
+ type SlashingProposerContract,
15
15
  type ViemCommitteeAttestations,
16
16
  type ViemHeader,
17
+ buildSimulationOverridesStateOverride,
17
18
  } from '@aztec/ethereum/contracts';
18
19
  import { type L1FeeAnalysisResult, L1FeeAnalyzer } from '@aztec/ethereum/l1-fee-analysis';
19
20
  import {
@@ -27,7 +28,6 @@ import {
27
28
  } from '@aztec/ethereum/l1-tx-utils';
28
29
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
29
30
  import { sumBigint } from '@aztec/foundation/bigint';
30
- import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
31
31
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
32
32
  import { trimmedBytesLength } from '@aztec/foundation/buffer';
33
33
  import { pick } from '@aztec/foundation/collection';
@@ -45,14 +45,12 @@ import { type ProposerSlashAction, encodeSlashConsensusVotes } from '@aztec/slas
45
45
  import { CommitteeAttestationsAndSigners, type ValidateCheckpointResult } from '@aztec/stdlib/block';
46
46
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
47
47
  import { getLastL1SlotTimestampForL2Slot, getNextL1SlotTimestamp } from '@aztec/stdlib/epoch-helpers';
48
- import { SlashFactoryContract } from '@aztec/stdlib/l1-contracts';
49
48
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
50
49
  import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats';
51
50
  import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
52
51
 
53
52
  import {
54
53
  type Hex,
55
- type StateOverride,
56
54
  type TransactionReceipt,
57
55
  type TypedDataDefinition,
58
56
  encodeFunctionData,
@@ -100,16 +98,13 @@ export const Actions = [
100
98
  'invalidate-by-insufficient-attestations',
101
99
  'propose',
102
100
  'governance-signal',
103
- 'empire-slashing-signal',
104
- 'create-empire-payload',
105
- 'execute-empire-payload',
106
101
  'vote-offenses',
107
102
  'execute-slash',
108
103
  ] as const;
109
104
 
110
105
  export type Action = (typeof Actions)[number];
111
106
 
112
- type GovernanceSignalAction = Extract<Action, 'governance-signal' | 'empire-slashing-signal'>;
107
+ type GovernanceSignalAction = Extract<Action, 'governance-signal'>;
113
108
 
114
109
  // Sorting for actions such that invalidations go before proposals, and proposals go before votes
115
110
  export const compareActions = (a: Action, b: Action) => Actions.indexOf(a) - Actions.indexOf(b);
@@ -124,12 +119,19 @@ export type InvalidateCheckpointRequest = {
124
119
  lastArchive: Fr;
125
120
  };
126
121
 
122
+ type EnqueueProposeCheckpointOpts = {
123
+ txTimeoutAt?: Date;
124
+ simulationOverridesPlan?: SimulationOverridesPlan;
125
+ };
126
+
127
127
  interface RequestWithExpiry {
128
128
  action: Action;
129
129
  request: L1TxRequest;
130
130
  lastValidL2Slot: SlotNumber;
131
131
  gasConfig?: Pick<L1TxConfig, 'txTimeoutAt' | 'gasLimit'>;
132
132
  blobConfig?: L1BlobInputs;
133
+ /** Optional pre-send validation. If it rejects, the request is discarded. */
134
+ preCheck?: () => Promise<void>;
133
135
  checkSuccess: (
134
136
  request: L1TxRequest,
135
137
  result?: { receipt: TransactionReceipt; stats?: TransactionStats; errorMsg?: string },
@@ -183,8 +185,7 @@ export class SequencerPublisher {
183
185
  public l1TxUtils: L1TxUtils;
184
186
  public rollupContract: RollupContract;
185
187
  public govProposerContract: GovernanceProposerContract;
186
- public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
187
- public slashFactoryContract: SlashFactoryContract;
188
+ public slashingProposerContract: SlashingProposerContract | undefined;
188
189
 
189
190
  public readonly tracer: Tracer;
190
191
 
@@ -198,9 +199,8 @@ export class SequencerPublisher {
198
199
  blobClient: BlobClientInterface;
199
200
  l1TxUtils: L1TxUtils;
200
201
  rollupContract: RollupContract;
201
- slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
202
+ slashingProposerContract: SlashingProposerContract | undefined;
202
203
  governanceProposerContract: GovernanceProposerContract;
203
- slashFactoryContract: SlashFactoryContract;
204
204
  epochCache: EpochCache;
205
205
  dateProvider: DateProvider;
206
206
  metrics: SequencerPublisherMetrics;
@@ -235,8 +235,6 @@ export class SequencerPublisher {
235
235
  const newSlashingProposer = await this.rollupContract.getSlashingProposer();
236
236
  this.slashingProposerContract = newSlashingProposer;
237
237
  });
238
- this.slashFactoryContract = deps.slashFactoryContract;
239
-
240
238
  // Initialize L1 fee analyzer for fisherman mode
241
239
  if (config.fishermanMode) {
242
240
  this.l1FeeAnalyzer = new L1FeeAnalyzer(
@@ -569,6 +567,28 @@ export class SequencerPublisher {
569
567
  if (this.interrupted) {
570
568
  return undefined;
571
569
  }
570
+
571
+ // Re-validate enqueued requests after the sleep (state may have changed, e.g. prune or L1 reorg)
572
+ const validRequests: RequestWithExpiry[] = [];
573
+ for (const request of this.requests) {
574
+ if (!request.preCheck) {
575
+ validRequests.push(request);
576
+ continue;
577
+ }
578
+
579
+ try {
580
+ await request.preCheck();
581
+ validRequests.push(request);
582
+ } catch (err) {
583
+ this.log.warn(`Pre-send validation failed for ${request.action}, discarding request`, err);
584
+ }
585
+ }
586
+
587
+ this.requests = validRequests;
588
+ if (this.requests.length === 0) {
589
+ return undefined;
590
+ }
591
+
572
592
  return this.sendRequests();
573
593
  }
574
594
 
@@ -647,27 +667,21 @@ export class SequencerPublisher {
647
667
  * @param tipArchive - The archive to check
648
668
  * @returns The slot and block number if it is possible to propose, undefined otherwise
649
669
  */
650
- public canProposeAt(
651
- tipArchive: Fr,
652
- msgSender: EthAddress,
653
- opts: {
654
- forcePendingCheckpointNumber?: CheckpointNumber;
655
- forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
656
- pipelined?: boolean;
657
- } = {},
658
- ) {
670
+ public async canProposeAt(tipArchive: Fr, msgSender: EthAddress, simulationOverridesPlan?: SimulationOverridesPlan) {
659
671
  // TODO: #14291 - should loop through multiple keys to check if any of them can propose
660
672
  const ignoredErrors = ['SlotAlreadyInChain', 'InvalidProposer', 'InvalidArchive'];
661
673
 
662
- const pipelined = opts.pipelined ?? this.epochCache.isProposerPipeliningEnabled();
674
+ const pipelined = this.epochCache.isProposerPipeliningEnabled();
663
675
  const slotOffset = pipelined ? this.aztecSlotDuration : 0n;
664
676
  const nextL1SlotTs = this.getNextL1SlotTimestamp() + slotOffset;
665
677
 
666
678
  return this.rollupContract
667
- .canProposeAt(tipArchive.toBuffer(), msgSender.toString(), nextL1SlotTs, {
668
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
669
- forceArchive: opts.forceArchive,
670
- })
679
+ .canProposeAt(
680
+ tipArchive.toBuffer(),
681
+ msgSender.toString(),
682
+ nextL1SlotTs,
683
+ await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan),
684
+ )
671
685
  .catch(err => {
672
686
  if (err instanceof FormattedViemError && ignoredErrors.find(e => err.message.includes(e))) {
673
687
  this.log.warn(`Failed canProposeAtTime check with ${ignoredErrors.find(e => err.message.includes(e))}`, {
@@ -689,7 +703,7 @@ export class SequencerPublisher {
689
703
  @trackSpan('SequencerPublisher.validateBlockHeader')
690
704
  public async validateBlockHeader(
691
705
  header: CheckpointHeader,
692
- opts?: { forcePendingCheckpointNumber: CheckpointNumber | undefined },
706
+ simulationOverridesPlan?: SimulationOverridesPlan,
693
707
  ): Promise<void> {
694
708
  const flags = { ignoreDA: true, ignoreSignatures: true };
695
709
 
@@ -704,9 +718,7 @@ export class SequencerPublisher {
704
718
  ] as const;
705
719
 
706
720
  const ts = this.getSimulationTimestamp(header.slotNumber);
707
- const stateOverrides = await this.rollupContract.makePendingCheckpointNumberOverride(
708
- opts?.forcePendingCheckpointNumber,
709
- );
721
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
710
722
  let balance = 0n;
711
723
  if (this.config.fishermanMode) {
712
724
  // In fisherman mode, we can't know where the proposer is publishing from
@@ -867,10 +879,7 @@ export class SequencerPublisher {
867
879
  checkpoint: Checkpoint,
868
880
  attestationsAndSigners: CommitteeAttestationsAndSigners,
869
881
  attestationsAndSignersSignature: Signature,
870
- options: {
871
- forcePendingCheckpointNumber?: CheckpointNumber;
872
- forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
873
- },
882
+ simulationOverridesPlan?: SimulationOverridesPlan,
874
883
  ): Promise<void> {
875
884
  const blobFields = checkpoint.toBlobFields();
876
885
  const blobs = await getBlobsPerL1Block(blobFields);
@@ -890,7 +899,7 @@ export class SequencerPublisher {
890
899
  blobInput,
891
900
  ] as const;
892
901
 
893
- await this.simulateProposeTx(args, options);
902
+ await this.simulateProposeTx(args, simulationOverridesPlan);
894
903
  }
895
904
 
896
905
  private async enqueueCastSignalHelper(
@@ -1078,55 +1087,6 @@ export class SequencerPublisher {
1078
1087
 
1079
1088
  for (const action of actions) {
1080
1089
  switch (action.type) {
1081
- case 'vote-empire-payload': {
1082
- if (this.slashingProposerContract?.type !== 'empire') {
1083
- this.log.error('Cannot vote for empire payload on non-empire slashing contract');
1084
- break;
1085
- }
1086
- this.log.debug(`Enqueuing slashing vote for payload ${action.payload} at slot ${slotNumber}`, {
1087
- signerAddress,
1088
- });
1089
- await this.enqueueCastSignalHelper(
1090
- slotNumber,
1091
- 'empire-slashing-signal',
1092
- action.payload,
1093
- this.slashingProposerContract,
1094
- signerAddress,
1095
- signer,
1096
- );
1097
- break;
1098
- }
1099
-
1100
- case 'create-empire-payload': {
1101
- this.log.debug(`Enqueuing slashing create payload at slot ${slotNumber}`, { slotNumber, signerAddress });
1102
- const request = this.slashFactoryContract.buildCreatePayloadRequest(action.data);
1103
- await this.simulateAndEnqueueRequest(
1104
- 'create-empire-payload',
1105
- request,
1106
- (receipt: TransactionReceipt) =>
1107
- !!this.slashFactoryContract.tryExtractSlashPayloadCreatedEvent(receipt.logs),
1108
- slotNumber,
1109
- );
1110
- break;
1111
- }
1112
-
1113
- case 'execute-empire-payload': {
1114
- this.log.debug(`Enqueuing slashing execute payload at slot ${slotNumber}`, { slotNumber, signerAddress });
1115
- if (this.slashingProposerContract?.type !== 'empire') {
1116
- this.log.error('Cannot execute slashing payload on non-empire slashing contract');
1117
- return false;
1118
- }
1119
- const empireSlashingProposer = this.slashingProposerContract as EmpireSlashingProposerContract;
1120
- const request = empireSlashingProposer.buildExecuteRoundRequest(action.round);
1121
- await this.simulateAndEnqueueRequest(
1122
- 'execute-empire-payload',
1123
- request,
1124
- (receipt: TransactionReceipt) => !!empireSlashingProposer.tryExtractPayloadSubmittedEvent(receipt.logs),
1125
- slotNumber,
1126
- );
1127
- break;
1128
- }
1129
-
1130
1090
  case 'vote-offenses': {
1131
1091
  this.log.debug(`Enqueuing slashing vote for ${action.votes.length} votes at slot ${slotNumber}`, {
1132
1092
  slotNumber,
@@ -1134,17 +1094,16 @@ export class SequencerPublisher {
1134
1094
  votesCount: action.votes.length,
1135
1095
  signerAddress,
1136
1096
  });
1137
- if (this.slashingProposerContract?.type !== 'tally') {
1138
- this.log.error('Cannot vote for slashing offenses on non-tally slashing contract');
1097
+ if (!this.slashingProposerContract) {
1098
+ this.log.error('No slashing proposer contract available');
1139
1099
  return false;
1140
1100
  }
1141
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1142
1101
  const votes = bufferToHex(encodeSlashConsensusVotes(action.votes));
1143
- const request = await tallySlashingProposer.buildVoteRequestFromSigner(votes, slotNumber, signer);
1102
+ const request = await this.slashingProposerContract.buildVoteRequestFromSigner(votes, slotNumber, signer);
1144
1103
  await this.simulateAndEnqueueRequest(
1145
1104
  'vote-offenses',
1146
1105
  request,
1147
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractVoteCastEvent(receipt.logs),
1106
+ (receipt: TransactionReceipt) => !!this.slashingProposerContract!.tryExtractVoteCastEvent(receipt.logs),
1148
1107
  slotNumber,
1149
1108
  );
1150
1109
  break;
@@ -1156,16 +1115,19 @@ export class SequencerPublisher {
1156
1115
  round: action.round,
1157
1116
  signerAddress,
1158
1117
  });
1159
- if (this.slashingProposerContract?.type !== 'tally') {
1160
- this.log.error('Cannot execute slashing offenses on non-tally slashing contract');
1118
+ if (!this.slashingProposerContract) {
1119
+ this.log.error('No slashing proposer contract available');
1161
1120
  return false;
1162
1121
  }
1163
- const tallySlashingProposer = this.slashingProposerContract as TallySlashingProposerContract;
1164
- const request = tallySlashingProposer.buildExecuteRoundRequest(action.round, action.committees);
1122
+ const executeRequest = this.slashingProposerContract.buildExecuteRoundRequest(
1123
+ action.round,
1124
+ action.committees,
1125
+ );
1165
1126
  await this.simulateAndEnqueueRequest(
1166
1127
  'execute-slash',
1167
- request,
1168
- (receipt: TransactionReceipt) => !!tallySlashingProposer.tryExtractRoundExecutedEvent(receipt.logs),
1128
+ executeRequest,
1129
+ (receipt: TransactionReceipt) =>
1130
+ !!this.slashingProposerContract!.tryExtractRoundExecutedEvent(receipt.logs),
1169
1131
  slotNumber,
1170
1132
  );
1171
1133
  break;
@@ -1186,11 +1148,7 @@ export class SequencerPublisher {
1186
1148
  checkpoint: Checkpoint,
1187
1149
  attestationsAndSigners: CommitteeAttestationsAndSigners,
1188
1150
  attestationsAndSignersSignature: Signature,
1189
- opts: {
1190
- txTimeoutAt?: Date;
1191
- forcePendingCheckpointNumber?: CheckpointNumber;
1192
- forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1193
- } = {},
1151
+ opts: EnqueueProposeCheckpointOpts = {},
1194
1152
  ): Promise<void> {
1195
1153
  const checkpointHeader = checkpoint.header;
1196
1154
 
@@ -1206,6 +1164,10 @@ export class SequencerPublisher {
1206
1164
  feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
1207
1165
  };
1208
1166
 
1167
+ const simulationOverridesPlan = SimulationOverridesBuilder.from(opts.simulationOverridesPlan)
1168
+ .withoutBlobCheck()
1169
+ .build();
1170
+
1209
1171
  try {
1210
1172
  // @note This will make sure that we are passing the checks for our header ASSUMING that the data is also made available
1211
1173
  // This means that we can avoid the simulation issues in later checks.
@@ -1216,19 +1178,43 @@ export class SequencerPublisher {
1216
1178
  checkpoint,
1217
1179
  attestationsAndSigners,
1218
1180
  attestationsAndSignersSignature,
1219
- opts,
1181
+ simulationOverridesPlan,
1220
1182
  );
1221
1183
  } catch (err: any) {
1222
1184
  this.log.error(`Checkpoint validation failed. ${err instanceof Error ? err.message : 'No error message'}`, err, {
1223
1185
  ...checkpoint.getStats(),
1224
1186
  slotNumber: checkpoint.header.slotNumber,
1225
- forcePendingCheckpointNumber: opts.forcePendingCheckpointNumber,
1187
+ simulationOverridesPlan,
1226
1188
  });
1227
1189
  throw err;
1228
1190
  }
1229
1191
 
1230
- this.log.verbose(`Enqueuing checkpoint propose transaction`, { ...checkpoint.toCheckpointInfo(), ...opts });
1231
- await this.addProposeTx(checkpoint, proposeTxArgs, opts);
1192
+ // Build a pre-check callback that re-validates the checkpoint before L1 submission.
1193
+ // During pipelining this catches stale proposals due to prunes or L1 reorgs that occur during the pipeline sleep.
1194
+ let preCheck = undefined;
1195
+ if (this.epochCache.isProposerPipeliningEnabled()) {
1196
+ preCheck = async () => {
1197
+ this.log.debug(`Re-validating checkpoint ${checkpoint.number} before L1 submission`);
1198
+ await this.validateCheckpointForSubmission(
1199
+ checkpoint,
1200
+ attestationsAndSigners,
1201
+ attestationsAndSignersSignature,
1202
+ simulationOverridesPlan,
1203
+ );
1204
+ };
1205
+ }
1206
+
1207
+ this.log.verbose(`Enqueuing checkpoint propose transaction`, {
1208
+ ...checkpoint.toCheckpointInfo(),
1209
+ txTimeoutAt: opts.txTimeoutAt,
1210
+ simulationOverridesPlan,
1211
+ });
1212
+ await this.addProposeTx(
1213
+ checkpoint,
1214
+ proposeTxArgs,
1215
+ { txTimeoutAt: opts.txTimeoutAt, simulationOverridesPlan },
1216
+ preCheck,
1217
+ );
1232
1218
  }
1233
1219
 
1234
1220
  public enqueueInvalidateCheckpoint(
@@ -1358,10 +1344,7 @@ export class SequencerPublisher {
1358
1344
  this.l1TxUtils.restart();
1359
1345
  }
1360
1346
 
1361
- private async prepareProposeTx(
1362
- encodedData: L1ProcessArgs,
1363
- options: { forcePendingCheckpointNumber?: CheckpointNumber },
1364
- ) {
1347
+ private async prepareProposeTx(encodedData: L1ProcessArgs, simulationOverridesPlan?: SimulationOverridesPlan) {
1365
1348
  const kzg = Blob.getViemKzgInstance();
1366
1349
  const blobInput = getPrefixedEthBlobCommitments(encodedData.blobs);
1367
1350
  this.log.debug('Validating blob input', { blobInput });
@@ -1432,7 +1415,7 @@ export class SequencerPublisher {
1432
1415
  blobInput,
1433
1416
  ] as const;
1434
1417
 
1435
- const { rollupData, simulationResult } = await this.simulateProposeTx(args, options);
1418
+ const { rollupData, simulationResult } = await this.simulateProposeTx(args, simulationOverridesPlan);
1436
1419
 
1437
1420
  return { args, blobEvaluationGas, rollupData, simulationResult };
1438
1421
  }
@@ -1456,10 +1439,7 @@ export class SequencerPublisher {
1456
1439
  ViemSignature,
1457
1440
  `0x${string}`,
1458
1441
  ],
1459
- options: {
1460
- forcePendingCheckpointNumber?: CheckpointNumber;
1461
- forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1462
- },
1442
+ simulationOverridesPlan?: SimulationOverridesPlan,
1463
1443
  ) {
1464
1444
  const rollupData = encodeFunctionData({
1465
1445
  abi: RollupAbi,
@@ -1467,34 +1447,7 @@ export class SequencerPublisher {
1467
1447
  args,
1468
1448
  });
1469
1449
 
1470
- // override the proposed checkpoint number if requested
1471
- const forcePendingCheckpointNumberStateDiff = (
1472
- options.forcePendingCheckpointNumber !== undefined
1473
- ? await this.rollupContract.makePendingCheckpointNumberOverride(options.forcePendingCheckpointNumber)
1474
- : []
1475
- ).flatMap(override => override.stateDiff ?? []);
1476
-
1477
- // override the fee header for a specific checkpoint number if requested (used when pipelining)
1478
- const forceProposedFeeHeaderStateDiff = (
1479
- options.forceProposedFeeHeader !== undefined
1480
- ? await this.rollupContract.makeFeeHeaderOverride(
1481
- options.forceProposedFeeHeader.checkpointNumber,
1482
- options.forceProposedFeeHeader.feeHeader,
1483
- )
1484
- : []
1485
- ).flatMap(override => override.stateDiff ?? []);
1486
-
1487
- const stateOverrides: StateOverride = [
1488
- {
1489
- address: this.rollupContract.address,
1490
- // @note we override checkBlob to false since blobs are not part simulate()
1491
- stateDiff: [
1492
- { slot: toPaddedHex(RollupContract.checkBlobStorageSlot, true), value: toPaddedHex(0n, true) },
1493
- ...forcePendingCheckpointNumberStateDiff,
1494
- ...forceProposedFeeHeaderStateDiff,
1495
- ],
1496
- },
1497
- ];
1450
+ const stateOverrides = await buildSimulationOverridesStateOverride(this.rollupContract, simulationOverridesPlan);
1498
1451
  // In fisherman mode, simulate as the proposer but with sufficient balance
1499
1452
  if (this.proposerAddressForSimulation) {
1500
1453
  stateOverrides.push({
@@ -1559,16 +1512,16 @@ export class SequencerPublisher {
1559
1512
  private async addProposeTx(
1560
1513
  checkpoint: Checkpoint,
1561
1514
  encodedData: L1ProcessArgs,
1562
- opts: {
1563
- txTimeoutAt?: Date;
1564
- forcePendingCheckpointNumber?: CheckpointNumber;
1565
- forceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
1566
- } = {},
1515
+ opts: EnqueueProposeCheckpointOpts = {},
1516
+ preCheck?: () => Promise<void>,
1567
1517
  ): Promise<void> {
1568
1518
  const slot = checkpoint.header.slotNumber;
1569
1519
  const timer = new Timer();
1570
1520
  const kzg = Blob.getViemKzgInstance();
1571
- const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(encodedData, opts);
1521
+ const { rollupData, simulationResult, blobEvaluationGas } = await this.prepareProposeTx(
1522
+ encodedData,
1523
+ opts.simulationOverridesPlan,
1524
+ );
1572
1525
  const startBlock = await this.l1TxUtils.getBlockNumber();
1573
1526
  const gasLimit = this.l1TxUtils.bumpGasLimit(
1574
1527
  BigInt(Math.ceil((Number(simulationResult.gasUsed) * 64) / 63)) +
@@ -1591,7 +1544,8 @@ export class SequencerPublisher {
1591
1544
  data: rollupData,
1592
1545
  },
1593
1546
  lastValidL2Slot: checkpoint.header.slotNumber,
1594
- gasConfig: { ...opts, gasLimit },
1547
+ gasConfig: { txTimeoutAt: opts.txTimeoutAt, gasLimit },
1548
+ preCheck,
1595
1549
  blobConfig: {
1596
1550
  blobs: encodedData.blobs.map(b => b.data),
1597
1551
  kzg,
@@ -1,8 +1,8 @@
1
1
  # Sequencer Timing Model
2
2
 
3
- The Aztec sequencer divides each slot into **fixed-duration sub-slots**. Each sub-slot has a pre-defined start and end time based on an initialization offset (how much time we expect syncing the previous slot will take), a finalization time (how much time we need for closing a checkpoint and publishing it to L1), and the configured block duration.
3
+ The Aztec sequencer divides each slot into **fixed-duration sub-slots**. Each sub-slot has a pre-defined start and end time based on an initialization offset (how much time we expect syncing the previous slot will take), the configured block duration, and whether checkpoint finalization is paid for in the current slot or deferred under proposer pipelining.
4
4
 
5
- **Example: 72-second slot with 8-second sub-slots**
5
+ **Example: 72-second slot with 8-second sub-slots (non-pipelined)**
6
6
 
7
7
  ```
8
8
  0s: Slot starts
@@ -31,7 +31,7 @@ Deadlines are fixed relative to slot start, not relative to when work actually c
31
31
 
32
32
  ## Overview
33
33
 
34
- The Aztec sequencer operates in fixed-duration **slots** (typically 72 seconds). During each slot, a designated proposer builds multiple **blocks** containing transactions over multiple **sub-slots**, then collects a single round of attestations for the entire **checkpoint** from validators, and finally publishes the resulting checkpoint to L1 Ethereum.
34
+ The Aztec sequencer operates in fixed-duration **slots** (typically 72 seconds). During each slot, a designated proposer builds multiple **blocks** containing transactions over multiple **sub-slots**. In the default mode, the same slot also reserves time to collect attestations for the resulting **checkpoint**, finalize it, and publish it to L1 Ethereum. When proposer pipelining is enabled, the slot budget for block building is larger because checkpoint finalization is deferred to the next target slot.
35
35
 
36
36
  ## Key Concepts
37
37
 
@@ -42,12 +42,14 @@ The Aztec sequencer operates in fixed-duration **slots** (typically 72 seconds).
42
42
  - **Checkpoint**: The collection of all blocks built in a slot, attested by validators and published to L1
43
43
  - **Sub-slot**: A fixed-duration time window within a slot (e.g., 8 seconds) during which a block should be built
44
44
 
45
- In a typical configuration, a 72-second slot contains:
45
+ In a typical configuration without pipelining, a 72-second slot contains:
46
46
  - 1 initialization period (2 seconds)
47
47
  - 5 block-building sub-slots (8 seconds each = 40 seconds)
48
48
  - 1 last validator re-execution sub-slot (8 seconds)
49
49
  - 1 attestation and publishing period (17 seconds)
50
50
 
51
+ With proposer pipelining enabled, the last validator re-execution sub-slot is still reserved, but the checkpoint finalization and L1 publishing budget is no longer subtracted when deciding how many block-building sub-slots fit in the slot.
52
+
51
53
  ### The Fixed Sub-Slot Model
52
54
 
53
55
  Building multiple blocks per slot uses **fixed sub-slots** with predictable deadlines:
@@ -75,14 +77,19 @@ These values are configurable but must satisfy certain constraints (explained be
75
77
 
76
78
  ## Calculating Sub-Slots and Blocks
77
79
 
78
- Given a slot configuration, we calculate how many blocks fit using this formula:
80
+ Given a slot configuration, we calculate how many blocks fit using these formulas:
79
81
 
80
82
  ```
81
- timeReservedAtEnd = blockDuration (last sub-slot for reexecution)
82
- + propagationTime (validators receive proposal)
83
- + propagationTime (attestations come back)
84
- + finalizationTime (checkpoint finalization)
85
- + l1PublishingTime (L1 transaction)
83
+ checkpointFinalizationTime = propagationTime
84
+ + propagationTime
85
+ + finalizationTime
86
+ + l1PublishingTime
87
+
88
+ timeReservedAtEnd (normal mode) = blockDuration (last sub-slot for reexecution)
89
+ + checkpointFinalizationTime
90
+
91
+ timeReservedAtEnd (pipelining) = assembleTime
92
+ + propagationTime (proposal must reach validators before the slot flips)
86
93
 
87
94
  timeAvailableForBlocks = slotDuration - initializationOffset - timeReservedAtEnd
88
95
 
@@ -101,6 +108,62 @@ This means:
101
108
  - Sub-slot 6: Reserved for validator re-execution of block 5
102
109
  - After sub-slot 6: Attestation collection, finalization, and L1 publishing
103
110
 
111
+ **The same slot with proposer pipelining enabled:**
112
+ ```
113
+ timeReservedAtEnd = 1s + 2s = 3s
114
+ timeAvailableForBlocks = 72s - 2s - 3s = 67s
115
+ numberOfBlocks = floor(67s / 8s) = 8 blocks
116
+ ```
117
+
118
+ The extra two block opportunities come from not charging the current slot for checkpoint finalization and L1 publishing.
119
+
120
+ ### Pipelining Mode
121
+
122
+ When proposer pipelining is enabled, the sequencer uses the current wall-clock slot to build the checkpoint for the **next target slot**.
123
+
124
+ It helps to think in terms of two different slots:
125
+
126
+ - **Wall-clock slot N-1**: The sequencer initializes checkpoint `N`, builds its blocks, and validators re-execute the last block
127
+ - **Target slot N**: Checkpoint `N` is proposed, attestations are gathered, and the L1 transaction is submitted
128
+
129
+ So the work is split like this:
130
+
131
+ - **During slot N-1**: Initialization, block building, and last-block re-execution
132
+ - **Near the end of slot N-1**: The checkpoint proposal is broadcast so validators can start the last re-execution as slot `N` begins.
133
+ - **During slot N**: Validators finish re-executing, send attestations, the proposer collects them, and the checkpoint is submitted to L1 before slot `N` reaches its publish cutoff
134
+
135
+ In other words, pipelining does not mean "do everything for slot N earlier". It specifically moves **block production and block re-execution** earlier, while **checkpoint proposal, attestation gathering, and L1 submission** remain aligned with slot `N`.
136
+
137
+ **Example: building checkpoint 12 while wall-clock time is in slot 11**
138
+ ```
139
+ Slot 11 (wall clock):
140
+ - Build blocks that will make up checkpoint 12
141
+ - Validators re-execute the last block of checkpoint 12
142
+ - Broadcast checkpoint 12 proposal
143
+ - Collect checkpoint 12 attestations
144
+
145
+ Slot 12 (target/submission slot):
146
+ - Collect attestations for checkpoint 12 until slot 12 reaches its L1 publish cutoff
147
+ - Submit checkpoint 12 to L1
148
+ ```
149
+
150
+ For timetable purposes, this changes two things:
151
+
152
+ - `maxNumberOfBlocks` is computed by reserving only the final validator re-execution sub-slot
153
+ - `initializeDeadline` no longer subtracts checkpoint finalization time; it only requires enough time for initialization, execution, and validator re-execution
154
+
155
+ In code, that means:
156
+
157
+ ```
158
+ initializeDeadline (normal mode) =
159
+ slotDuration - initializationOffset - 2 * minExecutionTime - checkpointFinalizationTime
160
+
161
+ initializeDeadline (pipelining) =
162
+ slotDuration - initializationOffset - 2 * minExecutionTime
163
+ ```
164
+
165
+ The fixed sub-slot deadlines themselves do not change. Pipelining only changes how much of the slot is considered available for block building.
166
+
104
167
  ## The Sequencer's Work
105
168
 
106
169
  When elected as proposer for a slot, the sequencer performs these tasks:
@@ -226,7 +289,9 @@ After the last block is built and validators have re-executed it:
226
289
 
227
290
  **Time reserved:** `2*propagationTime + finalizationTime + l1PublishingTime = 2s + 2s + 1s + 12s = 17s`
228
291
 
229
- This 17s comes after the last sub-slot, ensuring we have enough time to complete the checkpoint. If the sequencer receives the necessary attestations before the reserved time, the L1 tx is submitted earlier.
292
+ In the non-pipelined path, this 17s comes after the last sub-slot, ensuring we have enough time to complete the checkpoint. If the sequencer receives the necessary attestations before the reserved time, the L1 tx is submitted earlier.
293
+
294
+ With proposer pipelining enabled, this finalization budget is not charged against the current slot when calculating how many blocks fit. The checkpoint is instead queued for submission at the start of the target slot, so proposal broadcast, attestation gathering, and L1 submission happen in slot `N` while block building and block re-execution already happened in slot `N-1`.
230
295
 
231
296
  ## Handling Timing Variations
232
297
 
@@ -399,7 +464,7 @@ When configuring timing parameters, ensure these constraints are satisfied:
399
464
 
400
465
  ### Minimum Slot Duration
401
466
 
402
- For a valid configuration:
467
+ For a valid multi-block configuration without pipelining:
403
468
  ```
404
469
  slotDuration >= initializationOffset
405
470
  + blockDuration * 2 (at least 2 blocks)
@@ -414,6 +479,11 @@ Simplified:
414
479
  slotDuration >= initializationOffset + 3*blockDuration + 2*propagationTime + finalizationTime + l1PublishingTime
415
480
  ```
416
481
 
482
+ With proposer pipelining enabled, the same "at least 2 buildable blocks plus the final validator re-execution sub-slot" requirement becomes:
483
+ ```
484
+ slotDuration >= initializationOffset + 3*blockDuration
485
+ ```
486
+
417
487
  **Example:**
418
488
  ```
419
489
  slotDuration >= 2s + 3*8s + 2*2s + 1s + 12s = 2s + 24s + 4s + 1s + 12s = 43s
@@ -465,7 +535,7 @@ The sequencer transitions through these states during a slot:
465
535
  | **WAITING_UNTIL_NEXT_BLOCK** | Until next sub-slot start | Sleep between blocks to maintain intervals |
466
536
  | **ASSEMBLING_CHECKPOINT** | assembleTime (1s) | Assemble final checkpoint |
467
537
  | **COLLECTING_ATTESTATIONS** | Until L1 publish deadline | Wait for validator signatures |
468
- | **PUBLISHING_CHECKPOINT** | Until slot end | Submit to L1 |
538
+ | **PUBLISHING_CHECKPOINT** | Until L1 publish deadline | Submit to L1 |
469
539
 
470
540
  ## Complete Example: 72-Second Slot with 8-Second Sub-Slots
471
541