@aztec/sequencer-client 0.0.1-commit.f2ce05ee → 0.0.1-commit.f81dbcf

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 (54) hide show
  1. package/dest/client/sequencer-client.d.ts +23 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +99 -16
  4. package/dest/config.d.ts +24 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +36 -20
  7. package/dest/global_variable_builder/global_builder.d.ts +2 -4
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/publisher/config.d.ts +31 -17
  10. package/dest/publisher/config.d.ts.map +1 -1
  11. package/dest/publisher/config.js +101 -42
  12. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
  13. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  14. package/dest/publisher/sequencer-publisher-factory.js +13 -2
  15. package/dest/publisher/sequencer-publisher.d.ts +16 -7
  16. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher.js +41 -21
  18. package/dest/sequencer/checkpoint_proposal_job.d.ts +2 -4
  19. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  20. package/dest/sequencer/checkpoint_proposal_job.js +100 -56
  21. package/dest/sequencer/metrics.d.ts +17 -5
  22. package/dest/sequencer/metrics.d.ts.map +1 -1
  23. package/dest/sequencer/metrics.js +86 -15
  24. package/dest/sequencer/sequencer.d.ts +24 -13
  25. package/dest/sequencer/sequencer.d.ts.map +1 -1
  26. package/dest/sequencer/sequencer.js +36 -39
  27. package/dest/sequencer/timetable.d.ts +4 -3
  28. package/dest/sequencer/timetable.d.ts.map +1 -1
  29. package/dest/sequencer/timetable.js +6 -7
  30. package/dest/sequencer/types.d.ts +2 -2
  31. package/dest/sequencer/types.d.ts.map +1 -1
  32. package/dest/test/index.d.ts +3 -5
  33. package/dest/test/index.d.ts.map +1 -1
  34. package/dest/test/mock_checkpoint_builder.d.ts +14 -10
  35. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  36. package/dest/test/mock_checkpoint_builder.js +47 -34
  37. package/dest/test/utils.d.ts +3 -3
  38. package/dest/test/utils.d.ts.map +1 -1
  39. package/dest/test/utils.js +5 -4
  40. package/package.json +28 -28
  41. package/src/client/sequencer-client.ts +135 -18
  42. package/src/config.ts +45 -27
  43. package/src/global_variable_builder/global_builder.ts +1 -1
  44. package/src/publisher/config.ts +112 -43
  45. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  46. package/src/publisher/sequencer-publisher.ts +63 -28
  47. package/src/sequencer/checkpoint_proposal_job.ts +150 -66
  48. package/src/sequencer/metrics.ts +92 -18
  49. package/src/sequencer/sequencer.ts +45 -45
  50. package/src/sequencer/timetable.ts +7 -7
  51. package/src/sequencer/types.ts +1 -1
  52. package/src/test/index.ts +2 -4
  53. package/src/test/mock_checkpoint_builder.ts +64 -48
  54. 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,11 @@ import {
18
19
  type L1BlobInputs,
19
20
  type L1TxConfig,
20
21
  type L1TxRequest,
22
+ type L1TxUtils,
21
23
  MAX_L1_TX_LIMIT,
22
24
  type TransactionStats,
23
25
  WEI_CONST,
24
26
  } from '@aztec/ethereum/l1-tx-utils';
25
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
26
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
27
28
  import { sumBigint } from '@aztec/foundation/bigint';
28
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
@@ -32,6 +33,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254';
32
33
  import { EthAddress } from '@aztec/foundation/eth-address';
33
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
34
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
35
37
  import { bufferToHex } from '@aztec/foundation/string';
36
38
  import { DateProvider, Timer } from '@aztec/foundation/timer';
37
39
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -45,7 +47,7 @@ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from
45
47
 
46
48
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
47
49
 
48
- import type { PublisherConfig, TxSenderConfig } from './config.js';
50
+ import type { SequencerPublisherConfig } from './config.js';
49
51
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
50
52
 
51
53
  /** Arguments to the process method of the rollup contract */
@@ -60,6 +62,8 @@ type L1ProcessArgs = {
60
62
  attestationsAndSigners: CommitteeAttestationsAndSigners;
61
63
  /** Attestations and signers signature */
62
64
  attestationsAndSignersSignature: Signature;
65
+ /** The fee asset price modifier in basis points (from oracle) */
66
+ feeAssetPriceModifier: bigint;
63
67
  };
64
68
 
65
69
  export const Actions = [
@@ -112,6 +116,7 @@ export class SequencerPublisher {
112
116
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
113
117
 
114
118
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
119
+ private payloadProposedCache: Set<string> = new Set<string>();
115
120
 
116
121
  protected log: Logger;
117
122
  protected ethereumSlotDuration: bigint;
@@ -123,13 +128,17 @@ export class SequencerPublisher {
123
128
 
124
129
  /** L1 fee analyzer for fisherman mode */
125
130
  private l1FeeAnalyzer?: L1FeeAnalyzer;
131
+
132
+ /** Fee asset price oracle for computing price modifiers from Uniswap V4 */
133
+ private feeAssetPriceOracle: FeeAssetPriceOracle;
134
+
126
135
  // A CALL to a cold address is 2700 gas
127
136
  public static MULTICALL_OVERHEAD_GAS_GUESS = 5000n;
128
137
 
129
138
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
130
139
  public static VOTE_GAS_GUESS: bigint = 800_000n;
131
140
 
132
- public l1TxUtils: L1TxUtilsWithBlobs;
141
+ public l1TxUtils: L1TxUtils;
133
142
  public rollupContract: RollupContract;
134
143
  public govProposerContract: GovernanceProposerContract;
135
144
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -140,11 +149,12 @@ export class SequencerPublisher {
140
149
  protected requests: RequestWithExpiry[] = [];
141
150
 
142
151
  constructor(
143
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
152
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
153
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
144
154
  deps: {
145
155
  telemetry?: TelemetryClient;
146
156
  blobClient: BlobClientInterface;
147
- l1TxUtils: L1TxUtilsWithBlobs;
157
+ l1TxUtils: L1TxUtils;
148
158
  rollupContract: RollupContract;
149
159
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
150
160
  governanceProposerContract: GovernanceProposerContract;
@@ -188,12 +198,27 @@ export class SequencerPublisher {
188
198
  createLogger('sequencer:publisher:fee-analyzer'),
189
199
  );
190
200
  }
201
+
202
+ // Initialize fee asset price oracle
203
+ this.feeAssetPriceOracle = new FeeAssetPriceOracle(
204
+ this.l1TxUtils.client,
205
+ this.rollupContract,
206
+ createLogger('sequencer:publisher:price-oracle'),
207
+ );
191
208
  }
192
209
 
193
210
  public getRollupContract(): RollupContract {
194
211
  return this.rollupContract;
195
212
  }
196
213
 
214
+ /**
215
+ * Gets the fee asset price modifier from the oracle.
216
+ * Returns 0n if the oracle query fails.
217
+ */
218
+ public getFeeAssetPriceModifier(): Promise<bigint> {
219
+ return this.feeAssetPriceOracle.computePriceModifier();
220
+ }
221
+
197
222
  public getSenderAddress() {
198
223
  return this.l1TxUtils.getSenderAddress();
199
224
  }
@@ -617,24 +642,8 @@ export class SequencerPublisher {
617
642
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
618
643
  ): Promise<bigint> {
619
644
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
620
-
621
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
622
- // If we have no attestations, we still need to provide the empty attestations
623
- // so that the committee is recalculated correctly
624
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
625
- // if (ignoreSignatures) {
626
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
627
- // if (!committee) {
628
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
629
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
630
- // }
631
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
632
- // CommitteeAttestation.fromAddress(committeeMember),
633
- // );
634
- // }
635
-
636
645
  const blobFields = checkpoint.toBlobFields();
637
- const blobs = getBlobsPerL1Block(blobFields);
646
+ const blobs = await getBlobsPerL1Block(blobFields);
638
647
  const blobInput = getPrefixedEthBlobCommitments(blobs);
639
648
 
640
649
  const args = [
@@ -642,7 +651,7 @@ export class SequencerPublisher {
642
651
  header: checkpoint.header.toViem(),
643
652
  archive: toHex(checkpoint.archive.root.toBuffer()),
644
653
  oracleInput: {
645
- feeAssetPriceModifier: 0n,
654
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
646
655
  },
647
656
  },
648
657
  attestationsAndSigners.getPackedAttestations(),
@@ -691,6 +700,32 @@ export class SequencerPublisher {
691
700
  return false;
692
701
  }
693
702
 
703
+ // Check if payload was already submitted to governance
704
+ const cacheKey = payload.toString();
705
+ if (!this.payloadProposedCache.has(cacheKey)) {
706
+ try {
707
+ const l1StartBlock = await this.rollupContract.getL1StartBlock();
708
+ const proposed = await retry(
709
+ () => base.hasPayloadBeenProposed(payload.toString(), l1StartBlock),
710
+ 'Check if payload was proposed',
711
+ makeBackoff([0, 1, 2]),
712
+ this.log,
713
+ true,
714
+ );
715
+ if (proposed) {
716
+ this.payloadProposedCache.add(cacheKey);
717
+ }
718
+ } catch (err) {
719
+ this.log.warn(`Failed to check if payload ${payload} was proposed after retries, skipping signal`, err);
720
+ return false;
721
+ }
722
+ }
723
+
724
+ if (this.payloadProposedCache.has(cacheKey)) {
725
+ this.log.info(`Payload ${payload} was already proposed to governance, stopping signals`);
726
+ return false;
727
+ }
728
+
694
729
  const cachedLastVote = this.lastActions[signalType];
695
730
  this.lastActions[signalType] = slotNumber;
696
731
  const action = signalType;
@@ -918,14 +953,15 @@ export class SequencerPublisher {
918
953
  const checkpointHeader = checkpoint.header;
919
954
 
920
955
  const blobFields = checkpoint.toBlobFields();
921
- const blobs = getBlobsPerL1Block(blobFields);
956
+ const blobs = await getBlobsPerL1Block(blobFields);
922
957
 
923
- const proposeTxArgs = {
958
+ const proposeTxArgs: L1ProcessArgs = {
924
959
  header: checkpointHeader,
925
960
  archive: checkpoint.archive.root.toBuffer(),
926
961
  blobs,
927
962
  attestationsAndSigners,
928
963
  attestationsAndSignersSignature,
964
+ feeAssetPriceModifier: checkpoint.feeAssetPriceModifier,
929
965
  };
930
966
 
931
967
  let ts: bigint;
@@ -1113,8 +1149,7 @@ export class SequencerPublisher {
1113
1149
  header: encodedData.header.toViem(),
1114
1150
  archive: toHex(encodedData.archive),
1115
1151
  oracleInput: {
1116
- // We are currently not modifying these. See #9963
1117
- feeAssetPriceModifier: 0n,
1152
+ feeAssetPriceModifier: encodedData.feeAssetPriceModifier,
1118
1153
  },
1119
1154
  },
1120
1155
  encodedData.attestationsAndSigners.getPackedAttestations(),
@@ -1140,7 +1175,7 @@ export class SequencerPublisher {
1140
1175
  readonly header: ViemHeader;
1141
1176
  readonly archive: `0x${string}`;
1142
1177
  readonly oracleInput: {
1143
- readonly feeAssetPriceModifier: 0n;
1178
+ readonly feeAssetPriceModifier: bigint;
1144
1179
  };
1145
1180
  },
1146
1181
  ViemCommitteeAttestations,
@@ -1,5 +1,3 @@
1
- import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
2
- import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
3
1
  import type { EpochCache } from '@aztec/epoch-cache';
4
2
  import {
5
3
  BlockNumber,
@@ -9,6 +7,11 @@ import {
9
7
  SlotNumber,
10
8
  } from '@aztec/foundation/branded-types';
11
9
  import { randomInt } from '@aztec/foundation/crypto/random';
10
+ import {
11
+ flipSignature,
12
+ generateRecoverableSignature,
13
+ generateUnrecoverableSignature,
14
+ } from '@aztec/foundation/crypto/secp256k1-signer';
12
15
  import { Fr } from '@aztec/foundation/curves/bn254';
13
16
  import { EthAddress } from '@aztec/foundation/eth-address';
14
17
  import { Signature } from '@aztec/foundation/eth-signature';
@@ -27,18 +30,18 @@ import {
27
30
  type L2BlockSource,
28
31
  MaliciousCommitteeAttestationsAndSigners,
29
32
  } from '@aztec/stdlib/block';
30
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
33
+ import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
31
34
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
32
35
  import { Gas } from '@aztec/stdlib/gas';
33
36
  import {
34
- NoValidTxsError,
37
+ InsufficientValidTxsError,
35
38
  type PublicProcessorLimits,
36
39
  type ResolvedSequencerConfig,
37
40
  type WorldStateSynchronizer,
38
41
  } from '@aztec/stdlib/interfaces/server';
39
42
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
40
43
  import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
41
- import { orderAttestations } from '@aztec/stdlib/p2p';
44
+ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
42
45
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
43
46
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
44
47
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
@@ -129,7 +132,7 @@ export class CheckpointProposalJob implements Traceable {
129
132
  await Promise.all(votesPromises);
130
133
 
131
134
  if (checkpoint) {
132
- this.metrics.recordBlockProposalSuccess();
135
+ this.metrics.recordCheckpointProposalSuccess();
133
136
  }
134
137
 
135
138
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
@@ -186,18 +189,21 @@ export class CheckpointProposalJob implements Traceable {
186
189
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
187
190
 
188
191
  // Collect the out hashes of all the checkpoints before this one in the same epoch
189
- const previousCheckpoints = (await this.l2BlockSource.getCheckpointsForEpoch(this.epoch)).filter(
190
- c => c.number < this.checkpointNumber,
191
- );
192
- const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
192
+ const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
193
+ .filter(c => c.checkpointNumber < this.checkpointNumber)
194
+ .map(c => c.checkpointOutHash);
195
+
196
+ // Get the fee asset price modifier from the oracle
197
+ const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
193
198
 
194
199
  // Create a long-lived forked world state for the checkpoint builder
195
- using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
200
+ await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
196
201
 
197
202
  // Create checkpoint builder for the entire slot
198
203
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
199
204
  this.checkpointNumber,
200
205
  checkpointGlobalVariables,
206
+ feeAssetPriceModifier,
201
207
  l1ToL2Messages,
202
208
  previousCheckpointOutHashes,
203
209
  fork,
@@ -217,6 +223,7 @@ export class CheckpointProposalJob implements Traceable {
217
223
 
218
224
  let blocksInCheckpoint: L2Block[] = [];
219
225
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
226
+ const checkpointBuildTimer = new Timer();
220
227
 
221
228
  try {
222
229
  // Main loop: build blocks for the checkpoint
@@ -244,11 +251,44 @@ export class CheckpointProposalJob implements Traceable {
244
251
  return undefined;
245
252
  }
246
253
 
254
+ const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
255
+ if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
256
+ this.log.warn(
257
+ `Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
258
+ { slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
259
+ );
260
+ return undefined;
261
+ }
262
+
247
263
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
248
264
  // broadcasted yet, and wait to collect the committee attestations.
249
265
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
250
266
  const checkpoint = await checkpointBuilder.completeCheckpoint();
251
267
 
268
+ // Final validation round for the checkpoint before we propose it, just for safety
269
+ try {
270
+ validateCheckpoint(checkpoint, {
271
+ rollupManaLimit: this.l1Constants.rollupManaLimit,
272
+ maxL2BlockGas: this.config.maxL2BlockGas,
273
+ maxDABlockGas: this.config.maxDABlockGas,
274
+ maxTxsPerBlock: this.config.maxTxsPerBlock,
275
+ maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
276
+ });
277
+ } catch (err) {
278
+ this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
279
+ checkpoint: checkpoint.header.toInspect(),
280
+ });
281
+ return undefined;
282
+ }
283
+
284
+ // Record checkpoint-level build metrics
285
+ this.metrics.recordCheckpointBuild(
286
+ checkpointBuildTimer.ms(),
287
+ blocksInCheckpoint.length,
288
+ checkpoint.getStats().txCount,
289
+ Number(checkpoint.header.totalManaUsed.toBigInt()),
290
+ );
291
+
252
292
  // Do not collect attestations nor publish to L1 in fisherman mode
253
293
  if (this.config.fishermanMode) {
254
294
  this.log.info(
@@ -275,6 +315,7 @@ export class CheckpointProposalJob implements Traceable {
275
315
  const proposal = await this.validatorClient.createCheckpointProposal(
276
316
  checkpoint.header,
277
317
  checkpoint.archive.root,
318
+ feeAssetPriceModifier,
278
319
  lastBlock,
279
320
  this.proposer,
280
321
  checkpointProposalOptions,
@@ -313,6 +354,21 @@ export class CheckpointProposalJob implements Traceable {
313
354
  const aztecSlotDuration = this.l1Constants.slotDuration;
314
355
  const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
315
356
  const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
357
+
358
+ // If we have been configured to potentially skip publishing checkpoint then roll the dice here
359
+ if (
360
+ this.config.skipPublishingCheckpointsPercent !== undefined &&
361
+ this.config.skipPublishingCheckpointsPercent > 0
362
+ ) {
363
+ const result = Math.max(0, randomInt(100));
364
+ if (result < this.config.skipPublishingCheckpointsPercent) {
365
+ this.log.warn(
366
+ `Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
367
+ );
368
+ return checkpoint;
369
+ }
370
+ }
371
+
316
372
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
317
373
  txTimeoutAt,
318
374
  forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
@@ -347,9 +403,6 @@ export class CheckpointProposalJob implements Traceable {
347
403
  const txHashesAlreadyIncluded = new Set<string>();
348
404
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
349
405
 
350
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
351
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
352
-
353
406
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
354
407
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
355
408
 
@@ -382,7 +435,6 @@ export class CheckpointProposalJob implements Traceable {
382
435
  blockNumber,
383
436
  indexWithinCheckpoint,
384
437
  txHashesAlreadyIncluded,
385
- remainingBlobFields,
386
438
  });
387
439
 
388
440
  // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
@@ -408,12 +460,9 @@ export class CheckpointProposalJob implements Traceable {
408
460
  break;
409
461
  }
410
462
 
411
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
463
+ const { block, usedTxs } = buildResult;
412
464
  blocksInCheckpoint.push(block);
413
465
 
414
- // Update remaining blob fields for the next block
415
- remainingBlobFields = newRemainingBlobFields;
416
-
417
466
  // Sync the proposed block to the archiver to make it available
418
467
  // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
419
468
  // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
@@ -481,18 +530,10 @@ export class CheckpointProposalJob implements Traceable {
481
530
  indexWithinCheckpoint: IndexWithinCheckpoint;
482
531
  buildDeadline: Date | undefined;
483
532
  txHashesAlreadyIncluded: Set<string>;
484
- remainingBlobFields: number;
485
533
  },
486
- ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
487
- const {
488
- blockTimestamp,
489
- forceCreate,
490
- blockNumber,
491
- indexWithinCheckpoint,
492
- buildDeadline,
493
- txHashesAlreadyIncluded,
494
- remainingBlobFields,
495
- } = opts;
534
+ ): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
535
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
536
+ opts;
496
537
 
497
538
  this.log.verbose(
498
539
  `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
@@ -501,8 +542,7 @@ export class CheckpointProposalJob implements Traceable {
501
542
 
502
543
  try {
503
544
  // Wait until we have enough txs to build the block
504
- const minTxs = this.config.minTxsPerBlock;
505
- const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
545
+ const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
506
546
  if (!canStartBuilding) {
507
547
  this.log.warn(
508
548
  `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
@@ -516,7 +556,7 @@ export class CheckpointProposalJob implements Traceable {
516
556
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
517
557
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
518
558
  const pendingTxs = filter(
519
- this.p2pClient.iteratePendingTxs(),
559
+ this.p2pClient.iterateEligiblePendingTxs(),
520
560
  tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
521
561
  );
522
562
 
@@ -526,19 +566,24 @@ export class CheckpointProposalJob implements Traceable {
526
566
  );
527
567
  this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
528
568
 
529
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
530
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
531
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
532
-
533
- const blockBuilderOptions: PublicProcessorLimits = {
569
+ // Per-block limits derived at startup by computeBlockLimits(), further capped
570
+ // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
571
+ // minValidTxs is passed into the builder so it can reject the block *before* updating state.
572
+ const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
573
+ const blockBuilderOptions: PublicProcessorLimits & { minValidTxs?: number } = {
534
574
  maxTransactions: this.config.maxTxsPerBlock,
535
- maxBlockSize: this.config.maxBlockSizeInBytes,
536
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
537
- maxBlobFields: maxBlobFieldsForTxs,
575
+ maxBlockGas:
576
+ this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
577
+ ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
578
+ : undefined,
538
579
  deadline: buildDeadline,
580
+ isBuildingProposal: true,
581
+ minValidTxs,
539
582
  };
540
583
 
541
- // Actually build the block by executing txs
584
+ // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
585
+ // if the number of successfully processed txs is below minValidTxs, ensuring state is not
586
+ // updated for blocks that will be discarded.
542
587
  const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
543
588
  checkpointBuilder,
544
589
  pendingTxs,
@@ -550,14 +595,16 @@ export class CheckpointProposalJob implements Traceable {
550
595
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
551
596
  await this.dropFailedTxsFromP2P(buildResult.failedTxs);
552
597
 
553
- // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
554
- // too long, then we may not get to minTxsPerBlock after executing public functions.
555
- const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
556
- const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
557
- if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
598
+ if (buildResult.status === 'insufficient-valid-txs') {
558
599
  this.log.warn(
559
600
  `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
560
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
601
+ {
602
+ slot: this.slot,
603
+ blockNumber,
604
+ numTxs: buildResult.processedCount,
605
+ indexWithinCheckpoint,
606
+ minValidTxs,
607
+ },
561
608
  );
562
609
  this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
563
610
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
@@ -565,7 +612,7 @@ export class CheckpointProposalJob implements Traceable {
565
612
  }
566
613
 
567
614
  // Block creation succeeded, emit stats and metrics
568
- const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
615
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
569
616
 
570
617
  const blockStats = {
571
618
  eventName: 'l2-block-built',
@@ -576,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
576
623
 
577
624
  const blockHash = await block.hash();
578
625
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
579
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
626
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
580
627
 
581
628
  this.log.info(
582
629
  `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
@@ -584,9 +631,9 @@ export class CheckpointProposalJob implements Traceable {
584
631
  );
585
632
 
586
633
  this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
587
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
634
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
588
635
 
589
- return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
636
+ return { block, usedTxs };
590
637
  } catch (err: any) {
591
638
  this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
592
639
  this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
@@ -596,13 +643,13 @@ export class CheckpointProposalJob implements Traceable {
596
643
  }
597
644
  }
598
645
 
599
- /** Uses the checkpoint builder to build a block, catching specific txs */
646
+ /** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
600
647
  private async buildSingleBlockWithCheckpointBuilder(
601
648
  checkpointBuilder: CheckpointBuilder,
602
649
  pendingTxs: AsyncIterable<Tx>,
603
650
  blockNumber: BlockNumber,
604
651
  blockTimestamp: bigint,
605
- blockBuilderOptions: PublicProcessorLimits,
652
+ blockBuilderOptions: PublicProcessorLimits & { minValidTxs?: number },
606
653
  ) {
607
654
  try {
608
655
  const workTimer = new Timer();
@@ -610,8 +657,12 @@ export class CheckpointProposalJob implements Traceable {
610
657
  const blockBuildDuration = workTimer.ms();
611
658
  return { ...result, blockBuildDuration, status: 'success' as const };
612
659
  } catch (err: unknown) {
613
- if (isErrorClass(err, NoValidTxsError)) {
614
- return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
660
+ if (isErrorClass(err, InsufficientValidTxsError)) {
661
+ return {
662
+ failedTxs: err.failedTxs,
663
+ processedCount: err.processedCount,
664
+ status: 'insufficient-valid-txs' as const,
665
+ };
615
666
  }
616
667
  throw err;
617
668
  }
@@ -624,7 +675,7 @@ export class CheckpointProposalJob implements Traceable {
624
675
  blockNumber: BlockNumber;
625
676
  indexWithinCheckpoint: IndexWithinCheckpoint;
626
677
  buildDeadline: Date | undefined;
627
- }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
678
+ }): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
628
679
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
629
680
 
630
681
  // We only allow a block with 0 txs in the first block of the checkpoint
@@ -641,7 +692,7 @@ export class CheckpointProposalJob implements Traceable {
641
692
  // If we're past deadline, or we have no deadline, give up
642
693
  const now = this.dateProvider.nowAsDate();
643
694
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
644
- return { canStartBuilding: false, availableTxs: availableTxs };
695
+ return { canStartBuilding: false, availableTxs, minTxs };
645
696
  }
646
697
 
647
698
  // Wait a bit before checking again
@@ -654,7 +705,7 @@ export class CheckpointProposalJob implements Traceable {
654
705
  availableTxs = await this.p2pClient.getPendingTxCount();
655
706
  }
656
707
 
657
- return { canStartBuilding: true, availableTxs };
708
+ return { canStartBuilding: true, availableTxs, minTxs };
658
709
  }
659
710
 
660
711
  /**
@@ -706,11 +757,28 @@ export class CheckpointProposalJob implements Traceable {
706
757
 
707
758
  collectedAttestationsCount = attestations.length;
708
759
 
760
+ // Trim attestations to minimum required to save L1 calldata gas
761
+ const localAddresses = this.validatorClient.getValidatorAddresses();
762
+ const trimmed = trimAttestations(
763
+ attestations,
764
+ numberOfRequiredAttestations,
765
+ this.attestorAddress,
766
+ localAddresses,
767
+ );
768
+ if (trimmed.length < attestations.length) {
769
+ this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
770
+ }
771
+
709
772
  // Rollup contract requires that the signatures are provided in the order of the committee
710
- const sorted = orderAttestations(attestations, committee);
773
+ const sorted = orderAttestations(trimmed, committee);
711
774
 
712
775
  // Manipulate the attestations if we've been configured to do so
713
- if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
776
+ if (
777
+ this.config.injectFakeAttestation ||
778
+ this.config.injectHighSValueAttestation ||
779
+ this.config.injectUnrecoverableSignatureAttestation ||
780
+ this.config.shuffleAttestationOrdering
781
+ ) {
714
782
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
715
783
  }
716
784
 
@@ -739,7 +807,11 @@ export class CheckpointProposalJob implements Traceable {
739
807
  this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
740
808
  );
741
809
 
742
- if (this.config.injectFakeAttestation) {
810
+ if (
811
+ this.config.injectFakeAttestation ||
812
+ this.config.injectHighSValueAttestation ||
813
+ this.config.injectUnrecoverableSignatureAttestation
814
+ ) {
743
815
  // Find non-empty attestations that are not from the proposer
744
816
  const nonProposerIndices: number[] = [];
745
817
  for (let i = 0; i < attestations.length; i++) {
@@ -749,8 +821,20 @@ export class CheckpointProposalJob implements Traceable {
749
821
  }
750
822
  if (nonProposerIndices.length > 0) {
751
823
  const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
752
- this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
753
- unfreeze(attestations[targetIndex]).signature = Signature.random();
824
+ if (this.config.injectHighSValueAttestation) {
825
+ this.log.warn(
826
+ `Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
827
+ );
828
+ unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
829
+ } else if (this.config.injectUnrecoverableSignatureAttestation) {
830
+ this.log.warn(
831
+ `Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
832
+ );
833
+ unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
834
+ } else {
835
+ this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
836
+ unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
837
+ }
754
838
  }
755
839
  return new CommitteeAttestationsAndSigners(attestations);
756
840
  }
@@ -779,7 +863,7 @@ export class CheckpointProposalJob implements Traceable {
779
863
  const failedTxData = failedTxs.map(fail => fail.tx);
780
864
  const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
781
865
  this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
782
- await this.p2pClient.deleteTxs(failedTxHashes);
866
+ await this.p2pClient.handleFailedExecution(failedTxHashes);
783
867
  }
784
868
 
785
869
  /**
@@ -821,7 +905,7 @@ export class CheckpointProposalJob implements Traceable {
821
905
  slot: this.slot,
822
906
  feeAnalysisId: feeAnalysis?.id,
823
907
  });
824
- this.metrics.recordBlockProposalFailed('block_build_failed');
908
+ this.metrics.recordCheckpointProposalFailed('block_build_failed');
825
909
  }
826
910
 
827
911
  this.publisher.clearPendingRequests();