@aztec/sequencer-client 0.0.1-commit.1bea0213 → 0.0.1-commit.21ecf947b

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 (39) hide show
  1. package/dest/client/sequencer-client.js +1 -1
  2. package/dest/config.d.ts +1 -2
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +4 -8
  5. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  6. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  7. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  8. package/dest/publisher/sequencer-publisher.d.ts +8 -2
  9. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  10. package/dest/publisher/sequencer-publisher.js +53 -23
  11. package/dest/sequencer/checkpoint_proposal_job.d.ts +32 -9
  12. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  13. package/dest/sequencer/checkpoint_proposal_job.js +79 -51
  14. package/dest/sequencer/metrics.d.ts +6 -2
  15. package/dest/sequencer/metrics.d.ts.map +1 -1
  16. package/dest/sequencer/metrics.js +81 -22
  17. package/dest/sequencer/sequencer.d.ts +3 -1
  18. package/dest/sequencer/sequencer.d.ts.map +1 -1
  19. package/dest/sequencer/sequencer.js +7 -2
  20. package/dest/sequencer/timetable.d.ts +1 -4
  21. package/dest/sequencer/timetable.d.ts.map +1 -1
  22. package/dest/sequencer/timetable.js +1 -4
  23. package/dest/test/mock_checkpoint_builder.d.ts +7 -5
  24. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  25. package/dest/test/mock_checkpoint_builder.js +6 -6
  26. package/dest/test/utils.d.ts +3 -3
  27. package/dest/test/utils.d.ts.map +1 -1
  28. package/dest/test/utils.js +5 -4
  29. package/package.json +28 -28
  30. package/src/client/sequencer-client.ts +1 -1
  31. package/src/config.ts +9 -11
  32. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  33. package/src/publisher/sequencer-publisher.ts +60 -22
  34. package/src/sequencer/checkpoint_proposal_job.ts +104 -67
  35. package/src/sequencer/metrics.ts +89 -23
  36. package/src/sequencer/sequencer.ts +9 -2
  37. package/src/sequencer/timetable.ts +6 -5
  38. package/src/test/mock_checkpoint_builder.ts +14 -5
  39. package/src/test/utils.ts +5 -2
@@ -4,6 +4,7 @@ import type { EpochCache } from '@aztec/epoch-cache';
4
4
  import type { L1ContractsConfig } from '@aztec/ethereum/config';
5
5
  import {
6
6
  type EmpireSlashingProposerContract,
7
+ FeeAssetPriceOracle,
7
8
  type GovernanceProposerContract,
8
9
  type IEmpireBase,
9
10
  MULTI_CALL_3_ADDRESS,
@@ -18,11 +19,12 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ MAX_L1_TX_LIMIT,
21
23
  type TransactionStats,
22
24
  WEI_CONST,
23
25
  } from '@aztec/ethereum/l1-tx-utils';
24
26
  import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
25
- import { FormattedViemError, formatViemError, tryExtractEvent } from '@aztec/ethereum/utils';
27
+ import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
26
28
  import { sumBigint } from '@aztec/foundation/bigint';
27
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
28
30
  import { CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
@@ -59,6 +61,8 @@ type L1ProcessArgs = {
59
61
  attestationsAndSigners: CommitteeAttestationsAndSigners;
60
62
  /** Attestations and signers signature */
61
63
  attestationsAndSignersSignature: Signature;
64
+ /** The fee asset price modifier in basis points (from oracle) */
65
+ feeAssetPriceModifier: bigint;
62
66
  };
63
67
 
64
68
  export const Actions = [
@@ -122,10 +126,9 @@ export class SequencerPublisher {
122
126
 
123
127
  /** L1 fee analyzer for fisherman mode */
124
128
  private l1FeeAnalyzer?: L1FeeAnalyzer;
125
- // @note - with blobs, the below estimate seems too large.
126
- // Total used for full block from int_l1_pub e2e test: 1m (of which 86k is 1x blob)
127
- // Total used for emptier block from above test: 429k (of which 84k is 1x blob)
128
- public static PROPOSE_GAS_GUESS: bigint = 12_000_000n;
129
+
130
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
131
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
129
132
 
130
133
  // A CALL to a cold address is 2700 gas
131
134
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
@@ -192,12 +195,27 @@ export class SequencerPublisher {
192
195
  createLogger('sequencer:publisher:fee-analyzer'),
193
196
  );
194
197
  }
198
+
199
+ // Initialize fee asset price oracle
200
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
201
+ this.l1TxUtils.client,
202
+ this.rollupContract,
203
+ createLogger('sequencer:publisher:price-oracle'),
204
+ );
195
205
  }
196
206
 
197
207
  public getRollupContract(): RollupContract {
198
208
  return this.rollupContract;
199
209
  }
200
210
 
211
+ /**
212
+ * Gets the fee asset price modifier from the oracle.
213
+ * Returns 0n if the oracle query fails.
214
+ */
215
+ public getFeeAssetPriceModifier(): Promise<bigint> {
216
+ return this.feeAssetPriceOracle.computePriceModifier();
217
+ }
218
+
201
219
  public getSenderAddress() {
202
220
  return this.l1TxUtils.getSenderAddress();
203
221
  }
@@ -273,7 +291,7 @@ export class SequencerPublisher {
273
291
  // Start the analysis
274
292
  const analysisId = await this.l1FeeAnalyzer.startAnalysis(
275
293
  l2SlotNumber,
276
- gasLimit > 0n ? gasLimit : SequencerPublisher.PROPOSE_GAS_GUESS,
294
+ gasLimit > 0n ? gasLimit : MAX_L1_TX_LIMIT,
277
295
  l1Requests,
278
296
  blobConfig,
279
297
  onComplete,
@@ -346,7 +364,16 @@ export class SequencerPublisher {
346
364
 
347
365
  // Merge gasConfigs. Yields the sum of gasLimits, and the earliest txTimeoutAt, or undefined if no gasConfig sets them.
348
366
  const gasLimits = gasConfigs.map(g => g?.gasLimit).filter((g): g is bigint => g !== undefined);
349
- const gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
367
+ let gasLimit = gasLimits.length > 0 ? sumBigint(gasLimits) : undefined; // sum
368
+ // Cap at L1 block gas limit so the node accepts the tx ("gas limit too high" otherwise).
369
+ const maxGas = MAX_L1_TX_LIMIT;
370
+ if (gasLimit !== undefined && gasLimit > maxGas) {
371
+ this.log.debug('Capping bundled tx gas limit to L1 max', {
372
+ requested: gasLimit,
373
+ capped: maxGas,
374
+ });
375
+ gasLimit = maxGas;
376
+ }
350
377
  const txTimeoutAts = gasConfigs.map(g => g?.txTimeoutAt).filter((g): g is Date => g !== undefined);
351
378
  const txTimeoutAt = txTimeoutAts.length > 0 ? new Date(Math.min(...txTimeoutAts.map(g => g.getTime()))) : undefined; // earliest
352
379
  const txConfig: RequestWithExpiry['gasConfig'] = { gasLimit, txTimeoutAt };
@@ -517,7 +544,12 @@ export class SequencerPublisher {
517
544
  this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request });
518
545
 
519
546
  try {
520
- const { gasUsed } = await this.l1TxUtils.simulate(request, undefined, undefined, ErrorsAbi);
547
+ const { gasUsed } = await this.l1TxUtils.simulate(
548
+ request,
549
+ undefined,
550
+ undefined,
551
+ mergeAbis([request.abi ?? [], ErrorsAbi]),
552
+ );
521
553
  this.log.verbose(`Simulation for invalidate checkpoint ${checkpointNumber} succeeded`, {
522
554
  ...logData,
523
555
  request,
@@ -536,7 +568,7 @@ export class SequencerPublisher {
536
568
 
537
569
  // If the error is due to the checkpoint not being in the pending chain, and it was indeed removed by someone else,
538
570
  // we can safely ignore it and return undefined so we go ahead with checkpoint building.
539
- if (viemError.message?.includes('Rollup__BlockNotInPendingChain')) {
571
+ if (viemError.message?.includes('Rollup__CheckpointNotInPendingChain')) {
540
572
  this.log.verbose(
541
573
  `Simulation for invalidate checkpoint ${checkpointNumber} failed due to checkpoint not being in pending chain`,
542
574
  { ...logData, request, error: viemError.message },
@@ -632,7 +664,7 @@ export class SequencerPublisher {
632
664
  header: checkpoint.header.toViem(),
633
665
  archive: toHex(checkpoint.archive.root.toBuffer()),
634
666
  oracleInput: {
635
- feeAssetPriceModifier: 0n,
667
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
636
668
  },
637
669
  },
638
670
  attestationsAndSigners.getPackedAttestations(),
@@ -700,7 +732,7 @@ export class SequencerPublisher {
700
732
  });
701
733
 
702
734
  try {
703
- await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi);
735
+ await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi]));
704
736
  this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request });
705
737
  } catch (err) {
706
738
  this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err);
@@ -910,12 +942,13 @@ export class SequencerPublisher {
910
942
  const blobFields = checkpoint.toBlobFields();
911
943
  const blobs = getBlobsPerL1Block(blobFields);
912
944
 
913
- const proposeTxArgs = {
945
+ const proposeTxArgs: L1ProcessArgs = {
914
946
  header: checkpointHeader,
915
947
  archive: checkpoint.archive.root.toBuffer(),
916
948
  blobs,
917
949
  attestationsAndSigners,
918
950
  attestationsAndSignersSignature,
951
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
919
952
  };
920
953
 
921
954
  let ts: bigint;
@@ -999,12 +1032,14 @@ export class SequencerPublisher {
999
1032
  this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData);
1000
1033
 
1001
1034
  let gasUsed: bigint;
1035
+ const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]);
1002
1036
  try {
1003
- ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], ErrorsAbi)); // TODO(palla/slash): Check the timestamp logic
1037
+ ({ gasUsed } = await this.l1TxUtils.simulate(request, { time: timestamp }, [], simulateAbi)); // TODO(palla/slash): Check the timestamp logic
1004
1038
  this.log.verbose(`Simulation for ${action} succeeded`, { ...logData, request, gasUsed });
1005
1039
  } catch (err) {
1006
- const viemError = formatViemError(err);
1040
+ const viemError = formatViemError(err, simulateAbi);
1007
1041
  this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData);
1042
+
1008
1043
  return false;
1009
1044
  }
1010
1045
 
@@ -1012,10 +1047,14 @@ export class SequencerPublisher {
1012
1047
  const gasLimit = this.l1TxUtils.bumpGasLimit(BigInt(Math.ceil((Number(gasUsed) * 64) / 63)));
1013
1048
  logData.gasLimit = gasLimit;
1014
1049
 
1050
+ // Store the ABI used for simulation on the request so Multicall3.forward can decode errors
1051
+ // when the tx is sent and a revert is diagnosed via simulation.
1052
+ const requestWithAbi = { ...request, abi: simulateAbi };
1053
+
1015
1054
  this.log.debug(`Enqueuing ${action}`, logData);
1016
1055
  this.addRequest({
1017
1056
  action,
1018
- request,
1057
+ request: requestWithAbi,
1019
1058
  gasConfig: { gasLimit },
1020
1059
  lastValidL2Slot: slotNumber,
1021
1060
  checkSuccess: (_req, result) => {
@@ -1097,8 +1136,7 @@ export class SequencerPublisher {
1097
1136
  header: encodedData.header.toViem(),
1098
1137
  archive: toHex(encodedData.archive),
1099
1138
  oracleInput: {
1100
- // We are currently not modifying these. See #9963
1101
- feeAssetPriceModifier: 0n,
1139
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1102
1140
  },
1103
1141
  },
1104
1142
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1124,7 +1162,7 @@ export class SequencerPublisher {
1124
1162
  readonly header: ViemHeader;
1125
1163
  readonly archive: `0x${string}`;
1126
1164
  readonly oracleInput: {
1127
- readonly feeAssetPriceModifier: 0n;
1165
+ readonly feeAssetPriceModifier: bigint;
1128
1166
  };
1129
1167
  },
1130
1168
  ViemCommitteeAttestations,
@@ -1171,20 +1209,20 @@ export class SequencerPublisher {
1171
1209
  {
1172
1210
  to: this.rollupContract.address,
1173
1211
  data: rollupData,
1174
- gas: SequencerPublisher.PROPOSE_GAS_GUESS,
1212
+ gas: MAX_L1_TX_LIMIT,
1175
1213
  ...(this.proposerAddressForSimulation && { from: this.proposerAddressForSimulation.toString() }),
1176
1214
  },
1177
1215
  {
1178
1216
  // @note we add 1n to the timestamp because geth implementation doesn't like simulation timestamp to be equal to the current block timestamp
1179
1217
  time: timestamp + 1n,
1180
1218
  // @note reth should have a 30m gas limit per block but throws errors that this tx is beyond limit so we increase here
1181
- gasLimit: SequencerPublisher.PROPOSE_GAS_GUESS * 2n,
1219
+ gasLimit: MAX_L1_TX_LIMIT * 2n,
1182
1220
  },
1183
1221
  stateOverrides,
1184
1222
  RollupAbi,
1185
1223
  {
1186
1224
  // @note fallback gas estimate to use if the node doesn't support simulation API
1187
- fallbackGasEstimate: SequencerPublisher.PROPOSE_GAS_GUESS,
1225
+ fallbackGasEstimate: MAX_L1_TX_LIMIT,
1188
1226
  },
1189
1227
  )
1190
1228
  .catch(err => {
@@ -1194,7 +1232,7 @@ export class SequencerPublisher {
1194
1232
  this.log.debug(`Ignoring expected ValidatorSelection__MissingProposerSignature error in fisherman mode`);
1195
1233
  // Return a minimal simulation result with the fallback gas estimate
1196
1234
  return {
1197
- gasUsed: SequencerPublisher.PROPOSE_GAS_GUESS,
1235
+ gasUsed: MAX_L1_TX_LIMIT,
1198
1236
  logs: [],
1199
1237
  };
1200
1238
  }
@@ -1,16 +1,22 @@
1
1
  import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
2
2
  import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
- import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
+ import {
5
+ BlockNumber,
6
+ CheckpointNumber,
7
+ EpochNumber,
8
+ IndexWithinCheckpoint,
9
+ SlotNumber,
10
+ } from '@aztec/foundation/branded-types';
5
11
  import { randomInt } from '@aztec/foundation/crypto/random';
6
12
  import { Fr } from '@aztec/foundation/curves/bn254';
7
13
  import { EthAddress } from '@aztec/foundation/eth-address';
8
14
  import { Signature } from '@aztec/foundation/eth-signature';
9
15
  import { filter } from '@aztec/foundation/iterator';
10
- import type { Logger } from '@aztec/foundation/log';
16
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
11
17
  import { sleep, sleepUntil } from '@aztec/foundation/sleep';
12
18
  import { type DateProvider, Timer } from '@aztec/foundation/timer';
13
- import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
19
+ import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
14
20
  import type { P2P } from '@aztec/p2p';
15
21
  import type { SlasherClientInterface } from '@aztec/slasher';
16
22
  import {
@@ -24,10 +30,11 @@ import {
24
30
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
25
31
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
26
32
  import { Gas } from '@aztec/stdlib/gas';
27
- import type {
28
- PublicProcessorLimits,
29
- ResolvedSequencerConfig,
30
- WorldStateSynchronizer,
33
+ import {
34
+ NoValidTxsError,
35
+ type PublicProcessorLimits,
36
+ type ResolvedSequencerConfig,
37
+ type WorldStateSynchronizer,
31
38
  } from '@aztec/stdlib/interfaces/server';
32
39
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
33
40
  import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
@@ -59,6 +66,8 @@ const TXS_POLLING_MS = 500;
59
66
  * the Sequencer once the check for being the proposer for the slot has succeeded.
60
67
  */
61
68
  export class CheckpointProposalJob implements Traceable {
69
+ protected readonly log: Logger;
70
+
62
71
  constructor(
63
72
  private readonly epoch: EpochNumber,
64
73
  private readonly slot: SlotNumber,
@@ -86,9 +95,11 @@ export class CheckpointProposalJob implements Traceable {
86
95
  private readonly metrics: SequencerMetrics,
87
96
  private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
88
97
  private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
89
- protected readonly log: Logger,
90
98
  public readonly tracer: Tracer,
91
- ) {}
99
+ bindings?: LoggerBindings,
100
+ ) {
101
+ this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
102
+ }
92
103
 
93
104
  /**
94
105
  * Executes the checkpoint proposal job.
@@ -180,6 +191,9 @@ export class CheckpointProposalJob implements Traceable {
180
191
  );
181
192
  const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
182
193
 
194
+ // Get the fee asset price modifier from the oracle
195
+ const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
196
+
183
197
  // Create a long-lived forked world state for the checkpoint builder
184
198
  using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
185
199
 
@@ -187,9 +201,11 @@ export class CheckpointProposalJob implements Traceable {
187
201
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
188
202
  this.checkpointNumber,
189
203
  checkpointGlobalVariables,
204
+ feeAssetPriceModifier,
190
205
  l1ToL2Messages,
191
206
  previousCheckpointOutHashes,
192
207
  fork,
208
+ this.log.getBindings(),
193
209
  );
194
210
 
195
211
  // Options for the validator client when creating block and checkpoint proposals
@@ -220,19 +236,7 @@ export class CheckpointProposalJob implements Traceable {
220
236
  // These errors are expected in HA mode, so we yield and let another HA node handle the slot
221
237
  // The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
222
238
  // which is normal for block building (may have picked different txs)
223
- if (err instanceof DutyAlreadySignedError) {
224
- this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
225
- slot: this.slot,
226
- signedByNode: err.signedByNode,
227
- });
228
- return undefined;
229
- }
230
- if (err instanceof SlashingProtectionError) {
231
- this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
232
- slot: this.slot,
233
- existingMessageHash: err.existingMessageHash,
234
- attemptedMessageHash: err.attemptedMessageHash,
235
- });
239
+ if (this.handleHASigningError(err, 'Block proposal')) {
236
240
  return undefined;
237
241
  }
238
242
  throw err;
@@ -275,6 +279,7 @@ export class CheckpointProposalJob implements Traceable {
275
279
  const proposal = await this.validatorClient.createCheckpointProposal(
276
280
  checkpoint.header,
277
281
  checkpoint.archive.root,
282
+ feeAssetPriceModifier,
278
283
  lastBlock,
279
284
  this.proposer,
280
285
  checkpointProposalOptions,
@@ -301,20 +306,8 @@ export class CheckpointProposalJob implements Traceable {
301
306
  );
302
307
  } catch (err) {
303
308
  // We shouldn't really get here since we yield to another HA node
304
- // as soon as we see these errors when creating block proposals.
305
- if (err instanceof DutyAlreadySignedError) {
306
- this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
307
- slot: this.slot,
308
- signedByNode: err.signedByNode,
309
- });
310
- return undefined;
311
- }
312
- if (err instanceof SlashingProtectionError) {
313
- this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
314
- slot: this.slot,
315
- existingMessageHash: err.existingMessageHash,
316
- attemptedMessageHash: err.attemptedMessageHash,
317
- });
309
+ // as soon as we see these errors when creating block or checkpoint proposals.
310
+ if (this.handleHASigningError(err, 'Attestations signature')) {
318
311
  return undefined;
319
312
  }
320
313
  throw err;
@@ -367,7 +360,7 @@ export class CheckpointProposalJob implements Traceable {
367
360
 
368
361
  while (true) {
369
362
  const blocksBuilt = blocksInCheckpoint.length;
370
- const indexWithinCheckpoint = blocksBuilt;
363
+ const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
371
364
  const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
372
365
 
373
366
  const secondsIntoSlot = this.getSecondsIntoSlot();
@@ -397,6 +390,7 @@ export class CheckpointProposalJob implements Traceable {
397
390
  remainingBlobFields,
398
391
  });
399
392
 
393
+ // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
400
394
  if (!buildResult && timingInfo.isLastBlock) {
401
395
  // If no block was produced due to not enough txs and this was the last subslot, exit
402
396
  break;
@@ -483,13 +477,13 @@ export class CheckpointProposalJob implements Traceable {
483
477
 
484
478
  /** Builds a single block. Called from the main block building loop. */
485
479
  @trackSpan('CheckpointProposalJob.buildSingleBlock')
486
- private async buildSingleBlock(
480
+ protected async buildSingleBlock(
487
481
  checkpointBuilder: CheckpointBuilder,
488
482
  opts: {
489
483
  forceCreate?: boolean;
490
484
  blockTimestamp: bigint;
491
485
  blockNumber: BlockNumber;
492
- indexWithinCheckpoint: number;
486
+ indexWithinCheckpoint: IndexWithinCheckpoint;
493
487
  buildDeadline: Date | undefined;
494
488
  txHashesAlreadyIncluded: Set<string>;
495
489
  remainingBlobFields: number;
@@ -527,7 +521,7 @@ export class CheckpointProposalJob implements Traceable {
527
521
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
528
522
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
529
523
  const pendingTxs = filter(
530
- this.p2pClient.iteratePendingTxs(),
524
+ this.p2pClient.iterateEligiblePendingTxs(),
531
525
  tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
532
526
  );
533
527
 
@@ -550,45 +544,38 @@ export class CheckpointProposalJob implements Traceable {
550
544
  };
551
545
 
552
546
  // Actually build the block by executing txs
553
- const workTimer = new Timer();
554
- const {
555
- publicGas,
556
- block,
557
- publicProcessorDuration,
558
- numTxs,
559
- blockBuildingTimer,
560
- usedTxs,
561
- failedTxs,
562
- usedTxBlobFields,
563
- } = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
564
- const blockBuildDuration = workTimer.ms();
547
+ const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
548
+ checkpointBuilder,
549
+ pendingTxs,
550
+ blockNumber,
551
+ blockTimestamp,
552
+ blockBuilderOptions,
553
+ );
565
554
 
566
555
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
567
- await this.dropFailedTxsFromP2P(failedTxs);
556
+ await this.dropFailedTxsFromP2P(buildResult.failedTxs);
568
557
 
569
558
  // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
570
559
  // too long, then we may not get to minTxsPerBlock after executing public functions.
571
560
  const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
572
- if (!forceCreate && numTxs < minValidTxs) {
561
+ const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
562
+ if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
573
563
  this.log.warn(
574
- `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed (got ${numTxs} but required ${minValidTxs})`,
575
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint },
564
+ `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
565
+ { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
576
566
  );
577
- this.eventEmitter.emit('block-tx-count-check-failed', {
578
- minTxs: minValidTxs,
579
- availableTxs: numTxs,
580
- slot: this.slot,
581
- });
567
+ this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
582
568
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
583
569
  return undefined;
584
570
  }
585
571
 
586
572
  // Block creation succeeded, emit stats and metrics
573
+ const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
574
+
587
575
  const blockStats = {
588
576
  eventName: 'l2-block-built',
589
577
  duration: blockBuildDuration,
590
578
  publicProcessDuration: publicProcessorDuration,
591
- rollupCircuitsDuration: blockBuildingTimer.ms(),
592
579
  ...block.getStats(),
593
580
  } satisfies L2BlockBuiltStats;
594
581
 
@@ -614,17 +601,40 @@ export class CheckpointProposalJob implements Traceable {
614
601
  }
615
602
  }
616
603
 
604
+ /** Uses the checkpoint builder to build a block, catching specific txs */
605
+ private async buildSingleBlockWithCheckpointBuilder(
606
+ checkpointBuilder: CheckpointBuilder,
607
+ pendingTxs: AsyncIterable<Tx>,
608
+ blockNumber: BlockNumber,
609
+ blockTimestamp: bigint,
610
+ blockBuilderOptions: PublicProcessorLimits,
611
+ ) {
612
+ try {
613
+ const workTimer = new Timer();
614
+ const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
615
+ const blockBuildDuration = workTimer.ms();
616
+ return { ...result, blockBuildDuration, status: 'success' as const };
617
+ } catch (err: unknown) {
618
+ if (isErrorClass(err, NoValidTxsError)) {
619
+ return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
620
+ }
621
+ throw err;
622
+ }
623
+ }
624
+
617
625
  /** Waits until minTxs are available on the pool for building a block. */
618
626
  @trackSpan('CheckpointProposalJob.waitForMinTxs')
619
627
  private async waitForMinTxs(opts: {
620
628
  forceCreate?: boolean;
621
629
  blockNumber: BlockNumber;
622
- indexWithinCheckpoint: number;
630
+ indexWithinCheckpoint: IndexWithinCheckpoint;
623
631
  buildDeadline: Date | undefined;
624
632
  }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
625
- const minTxs = this.config.minTxsPerBlock;
626
633
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
627
634
 
635
+ // We only allow a block with 0 txs in the first block of the checkpoint
636
+ const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
637
+
628
638
  // Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
629
639
  const startBuildingDeadline = buildDeadline
630
640
  ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
@@ -645,7 +655,7 @@ export class CheckpointProposalJob implements Traceable {
645
655
  `Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
646
656
  { blockNumber, slot: this.slot, indexWithinCheckpoint },
647
657
  );
648
- await sleep(TXS_POLLING_MS);
658
+ await this.waitForTxsPollingInterval();
649
659
  availableTxs = await this.p2pClient.getPendingTxCount();
650
660
  }
651
661
 
@@ -686,7 +696,7 @@ export class CheckpointProposalJob implements Traceable {
686
696
  const attestationTimeAllowed = this.config.enforceTimeTable
687
697
  ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
688
698
  : this.l1Constants.slotDuration;
689
- const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
699
+ const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
690
700
 
691
701
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
692
702
 
@@ -774,7 +784,7 @@ export class CheckpointProposalJob implements Traceable {
774
784
  const failedTxData = failedTxs.map(fail => fail.tx);
775
785
  const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
776
786
  this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
777
- await this.p2pClient.deleteTxs(failedTxHashes);
787
+ await this.p2pClient.handleFailedExecution(failedTxHashes);
778
788
  }
779
789
 
780
790
  /**
@@ -822,6 +832,28 @@ export class CheckpointProposalJob implements Traceable {
822
832
  this.publisher.clearPendingRequests();
823
833
  }
824
834
 
835
+ /**
836
+ * Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
837
+ */
838
+ private handleHASigningError(err: any, errorContext: string): boolean {
839
+ if (err instanceof DutyAlreadySignedError) {
840
+ this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
841
+ slot: this.slot,
842
+ signedByNode: err.signedByNode,
843
+ });
844
+ return true;
845
+ }
846
+ if (err instanceof SlashingProtectionError) {
847
+ this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
848
+ slot: this.slot,
849
+ existingMessageHash: err.existingMessageHash,
850
+ attemptedMessageHash: err.attemptedMessageHash,
851
+ });
852
+ return true;
853
+ }
854
+ return false;
855
+ }
856
+
825
857
  /** Waits until a specific time within the current slot */
826
858
  @trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
827
859
  protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
@@ -830,6 +862,11 @@ export class CheckpointProposalJob implements Traceable {
830
862
  await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
831
863
  }
832
864
 
865
+ /** Waits the polling interval for transactions. Extracted for test overriding. */
866
+ protected async waitForTxsPollingInterval(): Promise<void> {
867
+ await sleep(TXS_POLLING_MS);
868
+ }
869
+
833
870
  private getSlotStartBuildTimestamp(): number {
834
871
  return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
835
872
  }