@aztec/validator-client 4.1.2 → 4.2.0-aztecnr-rc.2

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.
@@ -1,29 +1,20 @@
1
- import type { BlobClientInterface } from '@aztec/blob-client/client';
2
- import { type Blob, encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
3
1
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
4
2
  import type { EpochCache } from '@aztec/epoch-cache';
5
- import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
6
3
  import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
7
4
  import { pick } from '@aztec/foundation/collection';
8
5
  import { Fr } from '@aztec/foundation/curves/bn254';
9
6
  import { TimeoutError } from '@aztec/foundation/error';
10
- import type { LogData } from '@aztec/foundation/log';
11
7
  import { createLogger } from '@aztec/foundation/log';
12
8
  import { retryUntil } from '@aztec/foundation/retry';
13
9
  import { DateProvider, Timer } from '@aztec/foundation/timer';
14
10
  import type { P2P, PeerId } from '@aztec/p2p';
15
11
  import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
16
12
  import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
17
- import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
18
13
  import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
19
14
  import { Gas } from '@aztec/stdlib/gas';
20
15
  import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
21
- import {
22
- type L1ToL2MessageSource,
23
- accumulateCheckpointOutHashes,
24
- computeInHashFromL1ToL2Messages,
25
- } from '@aztec/stdlib/messaging';
26
- import type { BlockProposal, CheckpointProposalCore } from '@aztec/stdlib/p2p';
16
+ import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
17
+ import type { BlockProposal } from '@aztec/stdlib/p2p';
27
18
  import { MerkleTreeId } from '@aztec/stdlib/trees';
28
19
  import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx';
29
20
  import {
@@ -75,14 +66,11 @@ export type BlockProposalValidationFailureResult = {
75
66
 
76
67
  export type BlockProposalValidationResult = BlockProposalValidationSuccessResult | BlockProposalValidationFailureResult;
77
68
 
78
- export type CheckpointProposalValidationResult = { isValid: true } | { isValid: false; reason: string };
79
-
80
69
  type CheckpointComputationResult =
81
70
  | { checkpointNumber: CheckpointNumber; reason?: undefined }
82
71
  | { checkpointNumber?: undefined; reason: 'invalid_proposal' | 'global_variables_mismatch' };
83
72
 
84
- /** Handles block and checkpoint proposals for both validator and non-validator nodes. */
85
- export class ProposalHandler {
73
+ export class BlockProposalHandler {
86
74
  public readonly tracer: Tracer;
87
75
 
88
76
  constructor(
@@ -94,26 +82,21 @@ export class ProposalHandler {
94
82
  private blockProposalValidator: BlockProposalValidator,
95
83
  private epochCache: EpochCache,
96
84
  private config: ValidatorClientFullConfig,
97
- private blobClient: BlobClientInterface,
98
85
  private metrics?: ValidatorMetrics,
99
86
  private dateProvider: DateProvider = new DateProvider(),
100
87
  telemetry: TelemetryClient = getTelemetryClient(),
101
- private log = createLogger('validator:proposal-handler'),
88
+ private log = createLogger('validator:block-proposal-handler'),
102
89
  ) {
103
90
  if (config.fishermanMode) {
104
91
  this.log = this.log.createChild('[FISHERMAN]');
105
92
  }
106
- this.tracer = telemetry.getTracer('ProposalHandler');
93
+ this.tracer = telemetry.getTracer('BlockProposalHandler');
107
94
  }
108
95
 
109
- /**
110
- * Registers non-validator handlers for block and checkpoint proposals on the p2p client.
111
- * Block proposals are always registered. Checkpoint proposals are registered if the blob client can upload.
112
- */
113
- register(p2pClient: P2P, shouldReexecute: boolean): ProposalHandler {
96
+ register(p2pClient: P2P, shouldReexecute: boolean): BlockProposalHandler {
114
97
  // Non-validator handler that processes or re-executes for monitoring but does not attest.
115
98
  // Returns boolean indicating whether the proposal was valid.
116
- const blockHandler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
99
+ const handler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
117
100
  try {
118
101
  const { slotNumber, blockNumber } = proposal;
119
102
  const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
@@ -140,35 +123,7 @@ export class ProposalHandler {
140
123
  }
141
124
  };
142
125
 
143
- p2pClient.registerBlockProposalHandler(blockHandler);
144
-
145
- // Register checkpoint proposal handler if blob uploads are enabled and we are reexecuting
146
- if (this.blobClient.canUpload() && shouldReexecute) {
147
- const checkpointHandler = async (checkpoint: CheckpointProposalCore, _sender: PeerId) => {
148
- try {
149
- const proposalInfo = {
150
- proposalSlotNumber: checkpoint.slotNumber,
151
- archive: checkpoint.archive.toString(),
152
- proposer: checkpoint.getSender()?.toString(),
153
- };
154
- const result = await this.handleCheckpointProposal(checkpoint, proposalInfo);
155
- if (result.isValid) {
156
- this.log.info(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} handled`, proposalInfo);
157
- } else {
158
- this.log.warn(
159
- `Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} failed: ${result.reason}`,
160
- proposalInfo,
161
- );
162
- }
163
- } catch (error) {
164
- this.log.error('Error processing checkpoint proposal in non-validator handler', error);
165
- }
166
- // Non-validators don't attest
167
- return undefined;
168
- };
169
- p2pClient.registerCheckpointProposalHandler(checkpointHandler);
170
- }
171
-
126
+ p2pClient.registerBlockProposalHandler(handler);
172
127
  return this;
173
128
  }
174
129
 
@@ -614,6 +569,8 @@ export class ProposalHandler {
614
569
  ? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity)
615
570
  : undefined;
616
571
  const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
572
+ isBuildingProposal: false,
573
+ minValidTxs: 0,
617
574
  deadline,
618
575
  expectedEndState: blockHeader.state,
619
576
  maxTransactions: this.config.validateMaxTxsPerBlock,
@@ -668,234 +625,4 @@ export class ProposalHandler {
668
625
  totalManaUsed,
669
626
  };
670
627
  }
671
-
672
- /**
673
- * Validates a checkpoint proposal and uploads blobs if configured.
674
- * Used by both non-validator nodes (via register) and the validator client (via delegation).
675
- */
676
- async handleCheckpointProposal(
677
- proposal: CheckpointProposalCore,
678
- proposalInfo: LogData,
679
- ): Promise<CheckpointProposalValidationResult> {
680
- const proposer = proposal.getSender();
681
- if (!proposer) {
682
- this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
683
- return { isValid: false, reason: 'invalid_signature' };
684
- }
685
-
686
- if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
687
- this.log.warn(
688
- `Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`,
689
- );
690
- return { isValid: false, reason: 'invalid_fee_asset_price_modifier' };
691
- }
692
-
693
- const result = await this.validateCheckpointProposal(proposal, proposalInfo);
694
-
695
- // Upload blobs to filestore if validation passed (fire and forget)
696
- if (result.isValid) {
697
- this.tryUploadBlobsForCheckpoint(proposal, proposalInfo);
698
- }
699
-
700
- return result;
701
- }
702
-
703
- /**
704
- * Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
705
- * @returns Validation result with isValid flag and reason if invalid.
706
- */
707
- async validateCheckpointProposal(
708
- proposal: CheckpointProposalCore,
709
- proposalInfo: LogData,
710
- ): Promise<CheckpointProposalValidationResult> {
711
- const slot = proposal.slotNumber;
712
-
713
- // Timeout block syncing at the start of the next slot
714
- const config = this.checkpointsBuilder.getConfig();
715
- const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
716
- const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
717
-
718
- // Wait for last block to sync by archive
719
- let lastBlockHeader;
720
- try {
721
- lastBlockHeader = await retryUntil(
722
- async () => {
723
- await this.blockSource.syncImmediate();
724
- return this.blockSource.getBlockHeaderByArchive(proposal.archive);
725
- },
726
- `waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
727
- timeoutSeconds,
728
- 0.5,
729
- );
730
- } catch (err) {
731
- if (err instanceof TimeoutError) {
732
- this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
733
- return { isValid: false, reason: 'last_block_not_found' };
734
- }
735
- this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
736
- return { isValid: false, reason: 'block_fetch_error' };
737
- }
738
-
739
- if (!lastBlockHeader) {
740
- this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
741
- return { isValid: false, reason: 'last_block_not_found' };
742
- }
743
-
744
- // Get all full blocks for the slot and checkpoint
745
- const blocks = await this.blockSource.getBlocksForSlot(slot);
746
- if (blocks.length === 0) {
747
- this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
748
- return { isValid: false, reason: 'no_blocks_for_slot' };
749
- }
750
-
751
- // Ensure the last block for this slot matches the archive in the checkpoint proposal
752
- if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
753
- this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
754
- return { isValid: false, reason: 'last_block_archive_mismatch' };
755
- }
756
-
757
- this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
758
- ...proposalInfo,
759
- blockNumbers: blocks.map(b => b.number),
760
- });
761
-
762
- // Get checkpoint constants from first block
763
- const firstBlock = blocks[0];
764
- const constants = this.extractCheckpointConstants(firstBlock);
765
- const checkpointNumber = firstBlock.checkpointNumber;
766
-
767
- // Get L1-to-L2 messages for this checkpoint
768
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
769
-
770
- // Collect the out hashes of all the checkpoints before this one in the same epoch
771
- const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
772
- const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
773
- .filter(c => c.checkpointNumber < checkpointNumber)
774
- .map(c => c.checkpointOutHash);
775
-
776
- // Fork world state at the block before the first block
777
- const parentBlockNumber = BlockNumber(firstBlock.number - 1);
778
- const fork = await this.worldState.fork(parentBlockNumber);
779
-
780
- try {
781
- // Create checkpoint builder with all existing blocks
782
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
783
- checkpointNumber,
784
- constants,
785
- proposal.feeAssetPriceModifier,
786
- l1ToL2Messages,
787
- previousCheckpointOutHashes,
788
- fork,
789
- blocks,
790
- this.log.getBindings(),
791
- );
792
-
793
- // Complete the checkpoint to get computed values
794
- const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
795
-
796
- // Compare checkpoint header with proposal
797
- if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
798
- this.log.warn(`Checkpoint header mismatch`, {
799
- ...proposalInfo,
800
- computed: computedCheckpoint.header.toInspect(),
801
- proposal: proposal.checkpointHeader.toInspect(),
802
- });
803
- return { isValid: false, reason: 'checkpoint_header_mismatch' };
804
- }
805
-
806
- // Compare archive root with proposal
807
- if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
808
- this.log.warn(`Archive root mismatch`, {
809
- ...proposalInfo,
810
- computed: computedCheckpoint.archive.root.toString(),
811
- proposal: proposal.archive.toString(),
812
- });
813
- return { isValid: false, reason: 'archive_mismatch' };
814
- }
815
-
816
- // Check that the accumulated epoch out hash matches the value in the proposal.
817
- // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
818
- const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
819
- const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
820
- const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
821
- if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
822
- this.log.warn(`Epoch out hash mismatch`, {
823
- proposalEpochOutHash: proposalEpochOutHash.toString(),
824
- computedEpochOutHash: computedEpochOutHash.toString(),
825
- checkpointOutHash: checkpointOutHash.toString(),
826
- previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
827
- ...proposalInfo,
828
- });
829
- return { isValid: false, reason: 'out_hash_mismatch' };
830
- }
831
-
832
- // Final round of validations on the checkpoint, just in case.
833
- try {
834
- validateCheckpoint(computedCheckpoint, {
835
- rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
836
- maxDABlockGas: this.config.validateMaxDABlockGas,
837
- maxL2BlockGas: this.config.validateMaxL2BlockGas,
838
- maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
839
- maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint,
840
- });
841
- } catch (err) {
842
- this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
843
- return { isValid: false, reason: 'checkpoint_validation_failed' };
844
- }
845
-
846
- this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
847
- return { isValid: true };
848
- } finally {
849
- await fork.close();
850
- }
851
- }
852
-
853
- /** Extracts checkpoint global variables from a block. */
854
- private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
855
- const gv = block.header.globalVariables;
856
- return {
857
- chainId: gv.chainId,
858
- version: gv.version,
859
- slotNumber: gv.slotNumber,
860
- timestamp: gv.timestamp,
861
- coinbase: gv.coinbase,
862
- feeRecipient: gv.feeRecipient,
863
- gasFees: gv.gasFees,
864
- };
865
- }
866
-
867
- /** Triggers blob upload for a checkpoint if the blob client can upload (fire and forget). */
868
- protected tryUploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): void {
869
- if (this.blobClient.canUpload()) {
870
- void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
871
- }
872
- }
873
-
874
- /** Uploads blobs for a checkpoint to the filestore. */
875
- protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
876
- try {
877
- const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
878
- if (!lastBlockHeader) {
879
- this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
880
- return;
881
- }
882
-
883
- const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
884
- if (blocks.length === 0) {
885
- this.log.warn(`No blocks found for blob upload`, proposalInfo);
886
- return;
887
- }
888
-
889
- const blockBlobData = blocks.map(b => b.toBlockBlobData());
890
- const blobFields = encodeCheckpointBlobDataFromBlocks(blockBlobData);
891
- const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
892
- await this.blobClient.sendBlobsToFilestore(blobs);
893
- this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
894
- ...proposalInfo,
895
- numBlobs: blobs.length,
896
- });
897
- } catch (err) {
898
- this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
899
- }
900
- }
901
628
  }
@@ -20,6 +20,7 @@ import type { ContractDataSource } from '@aztec/stdlib/contract';
20
20
  import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
21
21
  import { Gas } from '@aztec/stdlib/gas';
22
22
  import {
23
+ type BlockBuilderOptions,
23
24
  type BuildBlockInCheckpointResult,
24
25
  type FullNodeBlockBuilderConfig,
25
26
  FullNodeBlockBuilderConfigKeys,
@@ -46,6 +47,9 @@ export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/serv
46
47
  export class CheckpointBuilder implements ICheckpointBlockBuilder {
47
48
  private log: Logger;
48
49
 
50
+ /** Persistent contracts DB shared across all blocks in this checkpoint. */
51
+ protected contractsDB: PublicContractsDB;
52
+
49
53
  constructor(
50
54
  private checkpointBuilder: LightweightCheckpointBuilder,
51
55
  private fork: MerkleTreeWriteOperations,
@@ -60,6 +64,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
60
64
  ...bindings,
61
65
  instanceId: `checkpoint-${checkpointBuilder.checkpointNumber}`,
62
66
  });
67
+ this.contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
63
68
  }
64
69
 
65
70
  getConstantData(): CheckpointGlobalVariables {
@@ -74,7 +79,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
74
79
  pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
75
80
  blockNumber: BlockNumber,
76
81
  timestamp: bigint,
77
- opts: PublicProcessorLimits & { expectedEndState?: StateReference; minValidTxs?: number } = {},
82
+ opts: BlockBuilderOptions & { expectedEndState?: StateReference },
78
83
  ): Promise<BuildBlockInCheckpointResult> {
79
84
  const slot = this.checkpointBuilder.constants.slotNumber;
80
85
 
@@ -104,6 +109,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
104
109
  ...this.capLimitsByCheckpointBudgets(opts),
105
110
  };
106
111
 
112
+ // Create a block-level checkpoint on the contracts DB so we can roll back on failure
113
+ this.contractsDB.createCheckpoint();
107
114
  // We execute all merkle tree operations on a world state fork checkpoint
108
115
  // This enables us to discard all modifications in the event that we fail to successfully process sufficient transactions
109
116
  const forkCheckpoint = await ForkCheckpoint.new(this.fork);
@@ -112,6 +119,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
112
119
  const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
113
120
  processor.process(pendingTxs, cappedOpts, validator),
114
121
  );
122
+
115
123
  // Throw before updating state if we don't have enough valid txs
116
124
  const minValidTxs = opts.minValidTxs ?? 0;
117
125
  if (processedTxs.length < minValidTxs) {
@@ -126,6 +134,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
126
134
  expectedEndState: opts.expectedEndState,
127
135
  });
128
136
 
137
+ this.contractsDB.commitCheckpoint();
138
+
129
139
  this.log.debug('Built block within checkpoint', {
130
140
  header: block.header.toInspect(),
131
141
  processedTxs: processedTxs.map(tx => tx.hash.toString()),
@@ -140,6 +150,8 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
140
150
  usedTxs,
141
151
  };
142
152
  } catch (err) {
153
+ // Revert all changes to contracts db
154
+ this.contractsDB.revertCheckpoint();
143
155
  // If we reached the point of committing the checkpoint, this does nothing
144
156
  // Otherwise it reverts any changes made to the fork for this failed block
145
157
  await forkCheckpoint.revert();
@@ -167,11 +179,12 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
167
179
 
168
180
  /**
169
181
  * Caps per-block gas and blob field limits by remaining checkpoint-level budgets.
170
- * Computes remaining L2 gas (mana), DA gas, and blob fields from blocks already added to the checkpoint,
171
- * then returns opts with maxBlockGas and maxBlobFields capped accordingly.
182
+ * When building a proposal (isBuildingProposal=true), computes a fair share of remaining budget
183
+ * across remaining blocks scaled by the multiplier. When validating, only caps by per-block limit
184
+ * and remaining checkpoint budget (no redistribution or multiplier).
172
185
  */
173
186
  protected capLimitsByCheckpointBudgets(
174
- opts: PublicProcessorLimits,
187
+ opts: BlockBuilderOptions,
175
188
  ): Pick<PublicProcessorLimits, 'maxBlockGas' | 'maxBlobFields' | 'maxTransactions'> {
176
189
  const existingBlocks = this.checkpointBuilder.getBlocks();
177
190
 
@@ -192,31 +205,31 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
192
205
  const blockEndOverhead = getNumBlockEndBlobFields(isFirstBlock);
193
206
  const maxBlobFieldsForTxs = totalBlobCapacity - usedBlobFields - blockEndOverhead;
194
207
 
195
- // Cap L2 gas by remaining checkpoint mana
196
- const cappedL2Gas = Math.min(opts.maxBlockGas?.l2Gas ?? remainingMana, remainingMana);
197
-
198
- // Cap DA gas by remaining checkpoint DA gas budget
199
- const cappedDAGas = Math.min(opts.maxBlockGas?.daGas ?? remainingDAGas, remainingDAGas);
200
-
201
- // Cap blob fields by remaining checkpoint blob capacity
202
- const cappedBlobFields =
203
- opts.maxBlobFields !== undefined ? Math.min(opts.maxBlobFields, maxBlobFieldsForTxs) : maxBlobFieldsForTxs;
204
-
205
- // Cap transaction count by remaining checkpoint tx budget
206
- let cappedMaxTransactions: number | undefined;
207
- if (this.config.maxTxsPerCheckpoint !== undefined) {
208
- const usedTxs = sum(existingBlocks.map(b => b.body.txEffects.length));
209
- const remainingTxs = Math.max(0, this.config.maxTxsPerCheckpoint - usedTxs);
210
- cappedMaxTransactions =
211
- opts.maxTransactions !== undefined ? Math.min(opts.maxTransactions, remainingTxs) : remainingTxs;
212
- } else {
213
- cappedMaxTransactions = opts.maxTransactions;
208
+ // Remaining txs
209
+ const usedTxs = sum(existingBlocks.map(b => b.body.txEffects.length));
210
+ const remainingTxs = Math.max(0, (this.config.maxTxsPerCheckpoint ?? Infinity) - usedTxs);
211
+
212
+ // Cap by per-block limit + remaining checkpoint budget
213
+ let cappedL2Gas = Math.min(opts.maxBlockGas?.l2Gas ?? Infinity, remainingMana);
214
+ let cappedDAGas = Math.min(opts.maxBlockGas?.daGas ?? Infinity, remainingDAGas);
215
+ let cappedBlobFields = Math.min(opts.maxBlobFields ?? Infinity, maxBlobFieldsForTxs);
216
+ let cappedMaxTransactions = Math.min(opts.maxTransactions ?? Infinity, remainingTxs);
217
+
218
+ // Proposer mode: further cap by fair share of remaining budget across remaining blocks
219
+ if (opts.isBuildingProposal) {
220
+ const remainingBlocks = Math.max(1, opts.maxBlocksPerCheckpoint - existingBlocks.length);
221
+ const multiplier = opts.perBlockAllocationMultiplier;
222
+
223
+ cappedL2Gas = Math.min(cappedL2Gas, Math.ceil((remainingMana / remainingBlocks) * multiplier));
224
+ cappedDAGas = Math.min(cappedDAGas, Math.ceil((remainingDAGas / remainingBlocks) * multiplier));
225
+ cappedBlobFields = Math.min(cappedBlobFields, Math.ceil((maxBlobFieldsForTxs / remainingBlocks) * multiplier));
226
+ cappedMaxTransactions = Math.min(cappedMaxTransactions, Math.ceil((remainingTxs / remainingBlocks) * multiplier));
214
227
  }
215
228
 
216
229
  return {
217
230
  maxBlockGas: new Gas(cappedDAGas, cappedL2Gas),
218
231
  maxBlobFields: cappedBlobFields,
219
- maxTransactions: cappedMaxTransactions,
232
+ maxTransactions: Number.isFinite(cappedMaxTransactions) ? cappedMaxTransactions : undefined,
220
233
  };
221
234
  }
222
235
 
@@ -225,7 +238,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
225
238
  ...(await getDefaultAllowedSetupFunctions()),
226
239
  ...(this.config.txPublicSetupAllowListExtend ?? []),
227
240
  ];
228
- const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
241
+ const contractsDB = this.contractsDB;
229
242
  const guardedFork = new GuardedMerkleTreeOperations(fork);
230
243
 
231
244
  const collectDebugLogs = this.debugLogStore.isEnabled;
package/src/factory.ts CHANGED
@@ -7,13 +7,14 @@ import type { L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
7
7
  import type { ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
8
8
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
9
9
  import type { TelemetryClient } from '@aztec/telemetry-client';
10
+ import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
10
11
 
12
+ import { BlockProposalHandler } from './block_proposal_handler.js';
11
13
  import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
12
14
  import { ValidatorMetrics } from './metrics.js';
13
- import { ProposalHandler } from './proposal_handler.js';
14
15
  import { ValidatorClient } from './validator.js';
15
16
 
16
- export function createProposalHandler(
17
+ export function createBlockProposalHandler(
17
18
  config: ValidatorClientFullConfig,
18
19
  deps: {
19
20
  checkpointsBuilder: FullNodeCheckpointsBuilder;
@@ -22,7 +23,6 @@ export function createProposalHandler(
22
23
  l1ToL2MessageSource: L1ToL2MessageSource;
23
24
  p2pClient: P2PClient;
24
25
  epochCache: EpochCache;
25
- blobClient: BlobClientInterface;
26
26
  dateProvider: DateProvider;
27
27
  telemetry: TelemetryClient;
28
28
  },
@@ -32,7 +32,7 @@ export function createProposalHandler(
32
32
  txsPermitted: !config.disableTransactions,
33
33
  maxTxsPerBlock: config.validateMaxTxsPerBlock ?? config.validateMaxTxsPerCheckpoint,
34
34
  });
35
- return new ProposalHandler(
35
+ return new BlockProposalHandler(
36
36
  deps.checkpointsBuilder,
37
37
  deps.worldState,
38
38
  deps.blockSource,
@@ -41,7 +41,6 @@ export function createProposalHandler(
41
41
  blockProposalValidator,
42
42
  deps.epochCache,
43
43
  config,
44
- deps.blobClient,
45
44
  metrics,
46
45
  deps.dateProvider,
47
46
  deps.telemetry,
@@ -61,6 +60,7 @@ export function createValidatorClient(
61
60
  epochCache: EpochCache;
62
61
  keyStoreManager: KeystoreManager | undefined;
63
62
  blobClient: BlobClientInterface;
63
+ slashingProtectionDb?: SlashingProtectionDatabase;
64
64
  },
65
65
  ) {
66
66
  if (config.disableValidator || !deps.keyStoreManager) {
@@ -81,5 +81,6 @@ export function createValidatorClient(
81
81
  deps.blobClient,
82
82
  deps.dateProvider,
83
83
  deps.telemetry,
84
+ deps.slashingProtectionDb,
84
85
  );
85
86
  }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * from './proposal_handler.js';
1
+ export * from './block_proposal_handler.js';
2
2
  export * from './checkpoint_builder.js';
3
3
  export * from './config.js';
4
4
  export * from './factory.js';
package/src/metrics.ts CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  createUpDownCounterWithDefault,
12
12
  } from '@aztec/telemetry-client';
13
13
 
14
- import type { BlockProposalValidationFailureReason } from './proposal_handler.js';
14
+ import type { BlockProposalValidationFailureReason } from './block_proposal_handler.js';
15
15
 
16
16
  export class ValidatorMetrics {
17
17
  private failedReexecutionCounter: UpDownCounter;