@aztec/sequencer-client 0.0.1-commit.1bb068fb5 → 0.0.1-commit.217f559981

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.d.ts +12 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +15 -4
  4. package/dest/config.d.ts +3 -3
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +13 -4
  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 +9 -7
  16. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher.js +27 -16
  18. package/dest/sequencer/checkpoint_proposal_job.d.ts +1 -1
  19. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  20. package/dest/sequencer/checkpoint_proposal_job.js +26 -6
  21. package/dest/sequencer/metrics.d.ts +14 -5
  22. package/dest/sequencer/metrics.d.ts.map +1 -1
  23. package/dest/sequencer/metrics.js +61 -15
  24. package/dest/sequencer/sequencer.d.ts +15 -7
  25. package/dest/sequencer/sequencer.d.ts.map +1 -1
  26. package/dest/sequencer/sequencer.js +23 -25
  27. package/dest/test/index.d.ts +3 -5
  28. package/dest/test/index.d.ts.map +1 -1
  29. package/package.json +28 -28
  30. package/src/client/sequencer-client.ts +25 -7
  31. package/src/config.ts +17 -8
  32. package/src/global_variable_builder/global_builder.ts +1 -1
  33. package/src/publisher/config.ts +112 -43
  34. package/src/publisher/sequencer-publisher-factory.ts +23 -6
  35. package/src/publisher/sequencer-publisher.ts +36 -23
  36. package/src/sequencer/checkpoint_proposal_job.ts +39 -7
  37. package/src/sequencer/metrics.ts +68 -18
  38. package/src/sequencer/sequencer.ts +31 -30
  39. package/src/test/index.ts +2 -4
@@ -1,32 +1,45 @@
1
1
  import { type BlobClientConfig, blobClientConfigMapping } from '@aztec/blob-client/client/config';
2
2
  import { type L1ReaderConfig, l1ReaderConfigMappings } from '@aztec/ethereum/l1-reader';
3
3
  import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from '@aztec/ethereum/l1-tx-utils/config';
4
- import {
5
- type ConfigMappingsType,
6
- SecretValue,
7
- booleanConfigHelper,
8
- getConfigFromMappings,
9
- } from '@aztec/foundation/config';
4
+ import { type ConfigMappingsType, SecretValue, booleanConfigHelper } from '@aztec/foundation/config';
10
5
  import { EthAddress } from '@aztec/foundation/eth-address';
11
6
 
12
- /**
13
- * The configuration of the rollup transaction publisher.
14
- */
7
+ /** Configuration of the transaction publisher. */
15
8
  export type TxSenderConfig = L1ReaderConfig & {
16
- /**
17
- * The private key to be used by the publisher.
18
- */
9
+ /** The private key to be used by the publisher. */
19
10
  publisherPrivateKeys?: SecretValue<`0x${string}`>[];
20
11
 
21
- /**
22
- * Publisher addresses to be used with a remote signer
23
- */
12
+ /** Publisher addresses to be used with a remote signer */
24
13
  publisherAddresses?: EthAddress[];
25
14
  };
26
15
 
27
- /**
28
- * Configuration of the L1Publisher.
29
- */
16
+ export type ProverTxSenderConfig = L1ReaderConfig & {
17
+ proverPublisherPrivateKeys?: SecretValue<`0x${string}`>[];
18
+ proverPublisherAddresses?: EthAddress[];
19
+ };
20
+
21
+ export type SequencerTxSenderConfig = L1ReaderConfig & {
22
+ sequencerPublisherPrivateKeys?: SecretValue<`0x${string}`>[];
23
+ sequencerPublisherAddresses?: EthAddress[];
24
+ };
25
+
26
+ export function getTxSenderConfigFromProverConfig(config: ProverTxSenderConfig): TxSenderConfig {
27
+ return {
28
+ ...config,
29
+ publisherPrivateKeys: config.proverPublisherPrivateKeys,
30
+ publisherAddresses: config.proverPublisherAddresses,
31
+ };
32
+ }
33
+
34
+ export function getTxSenderConfigFromSequencerConfig(config: SequencerTxSenderConfig): TxSenderConfig {
35
+ return {
36
+ ...config,
37
+ publisherPrivateKeys: config.sequencerPublisherPrivateKeys,
38
+ publisherAddresses: config.sequencerPublisherAddresses,
39
+ };
40
+ }
41
+
42
+ /** Configuration of the L1Publisher. */
30
43
  export type PublisherConfig = L1TxUtilsConfig &
31
44
  BlobClientConfig & {
32
45
  /** True to use publishers in invalid states (timed out, cancelled, etc) if no other is available */
@@ -37,35 +50,76 @@ export type PublisherConfig = L1TxUtilsConfig &
37
50
  publisherForwarderAddress?: EthAddress;
38
51
  };
39
52
 
40
- export const getTxSenderConfigMappings: (
41
- scope: 'PROVER' | 'SEQ',
42
- ) => ConfigMappingsType<Omit<TxSenderConfig, 'l1Contracts'>> = (scope: 'PROVER' | 'SEQ') => ({
53
+ export type ProverPublisherConfig = L1TxUtilsConfig &
54
+ BlobClientConfig & {
55
+ fishermanMode?: boolean;
56
+ proverPublisherAllowInvalidStates?: boolean;
57
+ proverPublisherForwarderAddress?: EthAddress;
58
+ };
59
+
60
+ export type SequencerPublisherConfig = L1TxUtilsConfig &
61
+ BlobClientConfig & {
62
+ fishermanMode?: boolean;
63
+ sequencerPublisherAllowInvalidStates?: boolean;
64
+ sequencerPublisherForwarderAddress?: EthAddress;
65
+ };
66
+
67
+ export function getPublisherConfigFromProverConfig(config: ProverPublisherConfig): PublisherConfig {
68
+ return {
69
+ ...config,
70
+ publisherAllowInvalidStates: config.proverPublisherAllowInvalidStates,
71
+ publisherForwarderAddress: config.proverPublisherForwarderAddress,
72
+ };
73
+ }
74
+
75
+ export function getPublisherConfigFromSequencerConfig(config: SequencerPublisherConfig): PublisherConfig {
76
+ return {
77
+ ...config,
78
+ publisherAllowInvalidStates: config.sequencerPublisherAllowInvalidStates,
79
+ publisherForwarderAddress: config.sequencerPublisherForwarderAddress,
80
+ };
81
+ }
82
+
83
+ export const proverTxSenderConfigMappings: ConfigMappingsType<Omit<ProverTxSenderConfig, 'l1Contracts'>> = {
43
84
  ...l1ReaderConfigMappings,
44
- publisherPrivateKeys: {
45
- env: scope === 'PROVER' ? `PROVER_PUBLISHER_PRIVATE_KEYS` : `SEQ_PUBLISHER_PRIVATE_KEYS`,
46
- description: 'The private keys to be used by the publisher.',
85
+ proverPublisherPrivateKeys: {
86
+ env: `PROVER_PUBLISHER_PRIVATE_KEYS`,
87
+ description: 'The private keys to be used by the prover publisher.',
47
88
  parseEnv: (val: string) => val.split(',').map(key => new SecretValue(`0x${key.replace('0x', '')}`)),
48
89
  defaultValue: [],
49
- fallback: [scope === 'PROVER' ? `PROVER_PUBLISHER_PRIVATE_KEY` : `SEQ_PUBLISHER_PRIVATE_KEY`],
90
+ fallback: [`PROVER_PUBLISHER_PRIVATE_KEY`],
50
91
  },
51
- publisherAddresses: {
52
- env: scope === 'PROVER' ? `PROVER_PUBLISHER_ADDRESSES` : `SEQ_PUBLISHER_ADDRESSES`,
92
+ proverPublisherAddresses: {
93
+ env: `PROVER_PUBLISHER_ADDRESSES`,
53
94
  description: 'The addresses of the publishers to use with remote signers',
54
95
  parseEnv: (val: string) => val.split(',').map(address => EthAddress.fromString(address)),
55
96
  defaultValue: [],
56
97
  },
57
- });
98
+ };
58
99
 
59
- export function getTxSenderConfigFromEnv(scope: 'PROVER' | 'SEQ'): Omit<TxSenderConfig, 'l1Contracts'> {
60
- return getConfigFromMappings(getTxSenderConfigMappings(scope));
61
- }
100
+ export const sequencerTxSenderConfigMappings: ConfigMappingsType<Omit<SequencerTxSenderConfig, 'l1Contracts'>> = {
101
+ ...l1ReaderConfigMappings,
102
+ sequencerPublisherPrivateKeys: {
103
+ env: `SEQ_PUBLISHER_PRIVATE_KEYS`,
104
+ description: 'The private keys to be used by the sequencer publisher.',
105
+ parseEnv: (val: string) => val.split(',').map(key => new SecretValue(`0x${key.replace('0x', '')}`)),
106
+ defaultValue: [],
107
+ fallback: [`SEQ_PUBLISHER_PRIVATE_KEY`],
108
+ },
109
+ sequencerPublisherAddresses: {
110
+ env: `SEQ_PUBLISHER_ADDRESSES`,
111
+ description: 'The addresses of the publishers to use with remote signers',
112
+ parseEnv: (val: string) => val.split(',').map(address => EthAddress.fromString(address)),
113
+ defaultValue: [],
114
+ },
115
+ };
62
116
 
63
- export const getPublisherConfigMappings: (
64
- scope: 'PROVER' | 'SEQ',
65
- ) => ConfigMappingsType<PublisherConfig & L1TxUtilsConfig> = scope => ({
66
- publisherAllowInvalidStates: {
117
+ export const sequencerPublisherConfigMappings: ConfigMappingsType<SequencerPublisherConfig & L1TxUtilsConfig> = {
118
+ ...l1TxUtilsConfigMappings,
119
+ ...blobClientConfigMapping,
120
+ sequencerPublisherAllowInvalidStates: {
121
+ env: `SEQ_PUBLISHER_ALLOW_INVALID_STATES`,
67
122
  description: 'True to use publishers in invalid states (timed out, cancelled, etc) if no other is available',
68
- env: scope === `PROVER` ? `PROVER_PUBLISHER_ALLOW_INVALID_STATES` : `SEQ_PUBLISHER_ALLOW_INVALID_STATES`,
69
123
  ...booleanConfigHelper(true),
70
124
  },
71
125
  fishermanMode: {
@@ -74,15 +128,30 @@ export const getPublisherConfigMappings: (
74
128
  'Whether to run in fisherman mode: builds blocks on every slot for validation without publishing to L1',
75
129
  ...booleanConfigHelper(false),
76
130
  },
77
- publisherForwarderAddress: {
78
- env: scope === `PROVER` ? `PROVER_PUBLISHER_FORWARDER_ADDRESS` : `SEQ_PUBLISHER_FORWARDER_ADDRESS`,
131
+ sequencerPublisherForwarderAddress: {
132
+ env: `SEQ_PUBLISHER_FORWARDER_ADDRESS`,
79
133
  description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
80
134
  parseEnv: (val: string) => (val ? EthAddress.fromString(val) : undefined),
81
135
  },
136
+ };
137
+
138
+ export const proverPublisherConfigMappings: ConfigMappingsType<ProverPublisherConfig & L1TxUtilsConfig> = {
82
139
  ...l1TxUtilsConfigMappings,
83
140
  ...blobClientConfigMapping,
84
- });
85
-
86
- export function getPublisherConfigFromEnv(scope: 'PROVER' | 'SEQ'): PublisherConfig {
87
- return getConfigFromMappings(getPublisherConfigMappings(scope));
88
- }
141
+ proverPublisherAllowInvalidStates: {
142
+ env: `PROVER_PUBLISHER_ALLOW_INVALID_STATES`,
143
+ description: 'True to use publishers in invalid states (timed out, cancelled, etc) if no other is available',
144
+ ...booleanConfigHelper(true),
145
+ },
146
+ fishermanMode: {
147
+ env: 'FISHERMAN_MODE',
148
+ description:
149
+ 'Whether to run in fisherman mode: builds blocks on every slot for validation without publishing to L1',
150
+ ...booleanConfigHelper(false),
151
+ },
152
+ proverPublisherForwarderAddress: {
153
+ env: `PROVER_PUBLISHER_FORWARDER_ADDRESS`,
154
+ description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
155
+ parseEnv: (val: string) => (val ? EthAddress.fromString(val) : undefined),
156
+ },
157
+ };
@@ -3,7 +3,7 @@ import { type Logger, createLogger } from '@aztec/aztec.js/log';
3
3
  import type { BlobClientInterface } from '@aztec/blob-client/client';
4
4
  import type { EpochCache } from '@aztec/epoch-cache';
5
5
  import type { GovernanceProposerContract, RollupContract } from '@aztec/ethereum/contracts';
6
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
6
+ import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
7
7
  import type { PublisherFilter, PublisherManager } from '@aztec/ethereum/publisher-manager';
8
8
  import { SlotNumber } from '@aztec/foundation/branded-types';
9
9
  import type { DateProvider } from '@aztec/foundation/timer';
@@ -26,13 +26,15 @@ export class SequencerPublisherFactory {
26
26
  /** Stores the last slot in which every action was carried out by a publisher */
27
27
  private lastActions: Partial<Record<Action, SlotNumber>> = {};
28
28
 
29
+ private nodeKeyStore: NodeKeystoreAdapter;
30
+
29
31
  private logger: Logger;
30
32
 
31
33
  constructor(
32
34
  private sequencerConfig: SequencerClientConfig,
33
35
  private deps: {
34
36
  telemetry: TelemetryClient;
35
- publisherManager: PublisherManager<L1TxUtilsWithBlobs>;
37
+ publisherManager: PublisherManager<L1TxUtils>;
36
38
  blobClient: BlobClientInterface;
37
39
  dateProvider: DateProvider;
38
40
  epochCache: EpochCache;
@@ -45,7 +47,17 @@ export class SequencerPublisherFactory {
45
47
  ) {
46
48
  this.publisherMetrics = new SequencerPublisherMetrics(deps.telemetry, 'SequencerPublisher');
47
49
  this.logger = deps.logger ?? createLogger('sequencer');
50
+ this.nodeKeyStore = this.deps.nodeKeyStore;
51
+ }
52
+
53
+ /**
54
+ * Updates the node keystore adapter used for publisher lookups.
55
+ * Called when the keystore is reloaded at runtime to reflect new validator-publisher mappings.
56
+ */
57
+ public updateNodeKeyStore(adapter: NodeKeystoreAdapter): void {
58
+ this.nodeKeyStore = adapter;
48
59
  }
60
+
49
61
  /**
50
62
  * Creates a new SequencerPublisher instance.
51
63
  * @param _validatorAddress - The address of the validator that will be using the publisher.
@@ -54,17 +66,17 @@ export class SequencerPublisherFactory {
54
66
  public async create(validatorAddress?: EthAddress): Promise<AttestorPublisherPair> {
55
67
  // If we have been given an attestor address we must only allow publishers permitted for that attestor
56
68
 
57
- const allowedPublishers = !validatorAddress ? [] : this.deps.nodeKeyStore.getPublisherAddresses(validatorAddress);
58
- const filter: PublisherFilter<L1TxUtilsWithBlobs> = !validatorAddress
69
+ const allowedPublishers = !validatorAddress ? [] : this.nodeKeyStore.getPublisherAddresses(validatorAddress);
70
+ const filter: PublisherFilter<L1TxUtils> = !validatorAddress
59
71
  ? () => true
60
- : (utils: L1TxUtilsWithBlobs) => {
72
+ : (utils: L1TxUtils) => {
61
73
  const publisherAddress = utils.getSenderAddress();
62
74
  return allowedPublishers.some(allowedPublisher => allowedPublisher.equals(publisherAddress));
63
75
  };
64
76
 
65
77
  const l1Publisher = await this.deps.publisherManager.getAvailablePublisher(filter);
66
78
  const attestorAddress =
67
- validatorAddress ?? this.deps.nodeKeyStore.getAttestorForPublisher(l1Publisher.getSenderAddress());
79
+ validatorAddress ?? this.nodeKeyStore.getAttestorForPublisher(l1Publisher.getSenderAddress());
68
80
 
69
81
  const rollup = this.deps.rollupContract;
70
82
  const slashingProposerContract = await rollup.getSlashingProposer();
@@ -89,4 +101,9 @@ export class SequencerPublisherFactory {
89
101
  publisher,
90
102
  };
91
103
  }
104
+
105
+ /** Interrupts all publishers managed by this factory. Used during sequencer shutdown. */
106
+ public interruptAll(): void {
107
+ this.deps.publisherManager.interrupt();
108
+ }
92
109
  }
@@ -19,11 +19,11 @@ import {
19
19
  type L1BlobInputs,
20
20
  type L1TxConfig,
21
21
  type L1TxRequest,
22
+ type L1TxUtils,
22
23
  MAX_L1_TX_LIMIT,
23
24
  type TransactionStats,
24
25
  WEI_CONST,
25
26
  } from '@aztec/ethereum/l1-tx-utils';
26
- import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
27
27
  import { FormattedViemError, formatViemError, mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
28
28
  import { sumBigint } from '@aztec/foundation/bigint';
29
29
  import { toHex as toPaddedHex } from '@aztec/foundation/bigint-buffer';
@@ -33,6 +33,7 @@ import type { Fr } from '@aztec/foundation/curves/bn254';
33
33
  import { EthAddress } from '@aztec/foundation/eth-address';
34
34
  import { Signature, type ViemSignature } from '@aztec/foundation/eth-signature';
35
35
  import { type Logger, createLogger } from '@aztec/foundation/log';
36
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
36
37
  import { bufferToHex } from '@aztec/foundation/string';
37
38
  import { DateProvider, Timer } from '@aztec/foundation/timer';
38
39
  import { EmpireBaseAbi, ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -46,7 +47,7 @@ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from
46
47
 
47
48
  import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem';
48
49
 
49
- import type { PublisherConfig, TxSenderConfig } from './config.js';
50
+ import type { SequencerPublisherConfig } from './config.js';
50
51
  import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js';
51
52
 
52
53
  /** Arguments to the process method of the rollup contract */
@@ -115,6 +116,7 @@ export class SequencerPublisher {
115
116
  protected lastActions: Partial<Record<Action, SlotNumber>> = {};
116
117
 
117
118
  private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
119
+ private payloadProposedCache: Set<string> = new Set<string>();
118
120
 
119
121
  protected log: Logger;
120
122
  protected ethereumSlotDuration: bigint;
@@ -136,7 +138,7 @@ export class SequencerPublisher {
136
138
  // Gas report for VotingWithSigTest shows a max gas of 100k, but we've seen it cost 700k+ in testnet
137
139
  public static VOTE_GAS_GUESS: bigint = 800_000n;
138
140
 
139
- public l1TxUtils: L1TxUtilsWithBlobs;
141
+ public l1TxUtils: L1TxUtils;
140
142
  public rollupContract: RollupContract;
141
143
  public govProposerContract: GovernanceProposerContract;
142
144
  public slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
@@ -147,11 +149,12 @@ export class SequencerPublisher {
147
149
  protected requests: RequestWithExpiry[] = [];
148
150
 
149
151
  constructor(
150
- private config: TxSenderConfig & PublisherConfig & Pick<L1ContractsConfig, 'ethereumSlotDuration'>,
152
+ private config: Pick<SequencerPublisherConfig, 'fishermanMode'> &
153
+ Pick<L1ContractsConfig, 'ethereumSlotDuration'> & { l1ChainId: number },
151
154
  deps: {
152
155
  telemetry?: TelemetryClient;
153
156
  blobClient: BlobClientInterface;
154
- l1TxUtils: L1TxUtilsWithBlobs;
157
+ l1TxUtils: L1TxUtils;
155
158
  rollupContract: RollupContract;
156
159
  slashingProposerContract: EmpireSlashingProposerContract | TallySlashingProposerContract | undefined;
157
160
  governanceProposerContract: GovernanceProposerContract;
@@ -639,24 +642,8 @@ export class SequencerPublisher {
639
642
  options: { forcePendingCheckpointNumber?: CheckpointNumber },
640
643
  ): Promise<bigint> {
641
644
  const ts = BigInt((await this.l1TxUtils.getBlock()).timestamp + this.ethereumSlotDuration);
642
-
643
- // TODO(palla/mbps): This should not be needed, there's no flow where we propose with zero attestations. Or is there?
644
- // If we have no attestations, we still need to provide the empty attestations
645
- // so that the committee is recalculated correctly
646
- // const ignoreSignatures = attestationsAndSigners.attestations.length === 0;
647
- // if (ignoreSignatures) {
648
- // const { committee } = await this.epochCache.getCommittee(block.header.globalVariables.slotNumber);
649
- // if (!committee) {
650
- // this.log.warn(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
651
- // throw new Error(`No committee found for slot ${block.header.globalVariables.slotNumber}`);
652
- // }
653
- // attestationsAndSigners.attestations = committee.map(committeeMember =>
654
- // CommitteeAttestation.fromAddress(committeeMember),
655
- // );
656
- // }
657
-
658
645
  const blobFields = checkpoint.toBlobFields();
659
- const blobs = getBlobsPerL1Block(blobFields);
646
+ const blobs = await getBlobsPerL1Block(blobFields);
660
647
  const blobInput = getPrefixedEthBlobCommitments(blobs);
661
648
 
662
649
  const args = [
@@ -713,6 +700,32 @@ export class SequencerPublisher {
713
700
  return false;
714
701
  }
715
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
+
716
729
  const cachedLastVote = this.lastActions[signalType];
717
730
  this.lastActions[signalType] = slotNumber;
718
731
  const action = signalType;
@@ -940,7 +953,7 @@ export class SequencerPublisher {
940
953
  const checkpointHeader = checkpoint.header;
941
954
 
942
955
  const blobFields = checkpoint.toBlobFields();
943
- const blobs = getBlobsPerL1Block(blobFields);
956
+ const blobs = await getBlobsPerL1Block(blobFields);
944
957
 
945
958
  const proposeTxArgs: L1ProcessArgs = {
946
959
  header: checkpointHeader,
@@ -129,7 +129,7 @@ export class CheckpointProposalJob implements Traceable {
129
129
  await Promise.all(votesPromises);
130
130
 
131
131
  if (checkpoint) {
132
- this.metrics.recordBlockProposalSuccess();
132
+ this.metrics.recordCheckpointProposalSuccess();
133
133
  }
134
134
 
135
135
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
@@ -186,16 +186,15 @@ export class CheckpointProposalJob implements Traceable {
186
186
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
187
187
 
188
188
  // 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());
189
+ const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
190
+ .filter(c => c.checkpointNumber < this.checkpointNumber)
191
+ .map(c => c.checkpointOutHash);
193
192
 
194
193
  // Get the fee asset price modifier from the oracle
195
194
  const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
196
195
 
197
196
  // Create a long-lived forked world state for the checkpoint builder
198
- using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
197
+ await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
199
198
 
200
199
  // Create checkpoint builder for the entire slot
201
200
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
@@ -221,6 +220,7 @@ export class CheckpointProposalJob implements Traceable {
221
220
 
222
221
  let blocksInCheckpoint: L2Block[] = [];
223
222
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
223
+ const checkpointBuildTimer = new Timer();
224
224
 
225
225
  try {
226
226
  // Main loop: build blocks for the checkpoint
@@ -248,11 +248,28 @@ export class CheckpointProposalJob implements Traceable {
248
248
  return undefined;
249
249
  }
250
250
 
251
+ const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
252
+ if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
253
+ this.log.warn(
254
+ `Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
255
+ { slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
256
+ );
257
+ return undefined;
258
+ }
259
+
251
260
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
252
261
  // broadcasted yet, and wait to collect the committee attestations.
253
262
  this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
254
263
  const checkpoint = await checkpointBuilder.completeCheckpoint();
255
264
 
265
+ // Record checkpoint-level build metrics
266
+ this.metrics.recordCheckpointBuild(
267
+ checkpointBuildTimer.ms(),
268
+ blocksInCheckpoint.length,
269
+ checkpoint.getStats().txCount,
270
+ Number(checkpoint.header.totalManaUsed.toBigInt()),
271
+ );
272
+
256
273
  // Do not collect attestations nor publish to L1 in fisherman mode
257
274
  if (this.config.fishermanMode) {
258
275
  this.log.info(
@@ -318,6 +335,21 @@ export class CheckpointProposalJob implements Traceable {
318
335
  const aztecSlotDuration = this.l1Constants.slotDuration;
319
336
  const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
320
337
  const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
338
+
339
+ // If we have been configured to potentially skip publishing checkpoint then roll the dice here
340
+ if (
341
+ this.config.skipPublishingCheckpointsPercent !== undefined &&
342
+ this.config.skipPublishingCheckpointsPercent > 0
343
+ ) {
344
+ const result = Math.max(0, randomInt(100));
345
+ if (result < this.config.skipPublishingCheckpointsPercent) {
346
+ this.log.warn(
347
+ `Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
348
+ );
349
+ return checkpoint;
350
+ }
351
+ }
352
+
321
353
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
322
354
  txTimeoutAt,
323
355
  forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
@@ -826,7 +858,7 @@ export class CheckpointProposalJob implements Traceable {
826
858
  slot: this.slot,
827
859
  feeAnalysisId: feeAnalysis?.id,
828
860
  });
829
- this.metrics.recordBlockProposalFailed('block_build_failed');
861
+ this.metrics.recordCheckpointProposalFailed('block_build_failed');
830
862
  }
831
863
 
832
864
  this.publisher.clearPendingRequests();