@aztec/validator-client 0.0.1-commit.7d4e6cd → 0.0.1-commit.7ffbba4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +95 -24
  2. package/dest/block_proposal_handler.d.ts +10 -10
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +76 -76
  5. package/dest/checkpoint_builder.d.ts +31 -25
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +114 -41
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +33 -14
  11. package/dest/duties/validation_service.d.ts +20 -7
  12. package/dest/duties/validation_service.d.ts.map +1 -1
  13. package/dest/duties/validation_service.js +69 -22
  14. package/dest/factory.d.ts +2 -2
  15. package/dest/factory.d.ts.map +1 -1
  16. package/dest/factory.js +3 -2
  17. package/dest/index.d.ts +1 -2
  18. package/dest/index.d.ts.map +1 -1
  19. package/dest/index.js +0 -1
  20. package/dest/key_store/ha_key_store.d.ts +99 -0
  21. package/dest/key_store/ha_key_store.d.ts.map +1 -0
  22. package/dest/key_store/ha_key_store.js +208 -0
  23. package/dest/key_store/index.d.ts +2 -1
  24. package/dest/key_store/index.d.ts.map +1 -1
  25. package/dest/key_store/index.js +1 -0
  26. package/dest/key_store/interface.d.ts +36 -6
  27. package/dest/key_store/interface.d.ts.map +1 -1
  28. package/dest/key_store/local_key_store.d.ts +10 -5
  29. package/dest/key_store/local_key_store.d.ts.map +1 -1
  30. package/dest/key_store/local_key_store.js +8 -4
  31. package/dest/key_store/node_keystore_adapter.d.ts +18 -5
  32. package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
  33. package/dest/key_store/node_keystore_adapter.js +18 -4
  34. package/dest/key_store/web3signer_key_store.d.ts +10 -5
  35. package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
  36. package/dest/key_store/web3signer_key_store.js +8 -4
  37. package/dest/metrics.d.ts +12 -3
  38. package/dest/metrics.d.ts.map +1 -1
  39. package/dest/metrics.js +46 -5
  40. package/dest/validator.d.ts +45 -18
  41. package/dest/validator.d.ts.map +1 -1
  42. package/dest/validator.js +262 -98
  43. package/package.json +21 -17
  44. package/src/block_proposal_handler.ts +93 -95
  45. package/src/checkpoint_builder.ts +171 -48
  46. package/src/config.ts +32 -13
  47. package/src/duties/validation_service.ts +94 -25
  48. package/src/factory.ts +2 -0
  49. package/src/index.ts +0 -1
  50. package/src/key_store/ha_key_store.ts +269 -0
  51. package/src/key_store/index.ts +1 -0
  52. package/src/key_store/interface.ts +44 -5
  53. package/src/key_store/local_key_store.ts +13 -4
  54. package/src/key_store/node_keystore_adapter.ts +27 -4
  55. package/src/key_store/web3signer_key_store.ts +17 -4
  56. package/src/metrics.ts +63 -6
  57. package/src/validator.ts +326 -116
  58. package/dest/tx_validator/index.d.ts +0 -3
  59. package/dest/tx_validator/index.d.ts.map +0 -1
  60. package/dest/tx_validator/index.js +0 -2
  61. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  62. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  63. package/dest/tx_validator/nullifier_cache.js +0 -24
  64. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  65. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  66. package/dest/tx_validator/tx_validator_factory.js +0 -53
  67. package/src/tx_validator/index.ts +0 -2
  68. package/src/tx_validator/nullifier_cache.ts +0 -30
  69. package/src/tx_validator/tx_validator_factory.ts +0 -133
@@ -1,10 +1,12 @@
1
+ import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
2
+ import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT } from '@aztec/constants';
1
3
  import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
- import { merge, pick } from '@aztec/foundation/collection';
4
+ import { merge, pick, sum } from '@aztec/foundation/collection';
3
5
  import { Fr } from '@aztec/foundation/curves/bn254';
4
- import { createLogger } from '@aztec/foundation/log';
6
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
5
7
  import { bufferToHex } from '@aztec/foundation/string';
6
- import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
7
- import { getDefaultAllowedSetupFunctions } from '@aztec/p2p/msg_validators';
8
+ import { DateProvider, elapsed } from '@aztec/foundation/timer';
9
+ import { createTxValidatorForBlockBuilding, getDefaultAllowedSetupFunctions } from '@aztec/p2p/msg_validators';
8
10
  import { LightweightCheckpointBuilder } from '@aztec/prover-client/light';
9
11
  import {
10
12
  GuardedMerkleTreeOperations,
@@ -12,39 +14,37 @@ import {
12
14
  PublicProcessor,
13
15
  createPublicTxSimulatorForBlockBuilding,
14
16
  } from '@aztec/simulator/server';
15
- import { L2BlockNew } from '@aztec/stdlib/block';
17
+ import { L2Block } from '@aztec/stdlib/block';
16
18
  import { Checkpoint } from '@aztec/stdlib/checkpoint';
17
19
  import type { ContractDataSource } from '@aztec/stdlib/contract';
20
+ import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
18
21
  import { Gas } from '@aztec/stdlib/gas';
19
22
  import {
23
+ type BuildBlockInCheckpointResult,
20
24
  type FullNodeBlockBuilderConfig,
21
25
  FullNodeBlockBuilderConfigKeys,
26
+ type ICheckpointBlockBuilder,
27
+ type ICheckpointsBuilder,
22
28
  type MerkleTreeWriteOperations,
29
+ NoValidTxsError,
23
30
  type PublicProcessorLimits,
31
+ type WorldStateSynchronizer,
24
32
  } from '@aztec/stdlib/interfaces/server';
33
+ import { type DebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs';
25
34
  import { MerkleTreeId } from '@aztec/stdlib/trees';
26
- import { type CheckpointGlobalVariables, type FailedTx, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
35
+ import { type CheckpointGlobalVariables, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
27
36
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
28
37
 
29
- import { createValidatorForBlockBuilding } from './tx_validator/tx_validator_factory.js';
30
-
31
- const log = createLogger('checkpoint-builder');
32
-
33
- export interface BuildBlockInCheckpointResult {
34
- block: L2BlockNew;
35
- publicGas: Gas;
36
- publicProcessorDuration: number;
37
- numTxs: number;
38
- failedTxs: FailedTx[];
39
- blockBuildingTimer: Timer;
40
- usedTxs: Tx[];
41
- }
38
+ // Re-export for backward compatibility
39
+ export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/server';
42
40
 
43
41
  /**
44
42
  * Builder for a single checkpoint. Handles building blocks within the checkpoint
45
43
  * and completing it.
46
44
  */
47
- export class CheckpointBuilder {
45
+ export class CheckpointBuilder implements ICheckpointBlockBuilder {
46
+ private log: Logger;
47
+
48
48
  constructor(
49
49
  private checkpointBuilder: LightweightCheckpointBuilder,
50
50
  private fork: MerkleTreeWriteOperations,
@@ -52,7 +52,14 @@ export class CheckpointBuilder {
52
52
  private contractDataSource: ContractDataSource,
53
53
  private dateProvider: DateProvider,
54
54
  private telemetryClient: TelemetryClient,
55
- ) {}
55
+ bindings?: LoggerBindings,
56
+ private debugLogStore: DebugLogStore = new NullDebugLogStore(),
57
+ ) {
58
+ this.log = createLogger('checkpoint-builder', {
59
+ ...bindings,
60
+ instanceId: `checkpoint-${checkpointBuilder.checkpointNumber}`,
61
+ });
62
+ }
56
63
 
57
64
  getConstantData(): CheckpointGlobalVariables {
58
65
  return this.checkpointBuilder.constants;
@@ -60,17 +67,22 @@ export class CheckpointBuilder {
60
67
 
61
68
  /**
62
69
  * Builds a single block within this checkpoint.
70
+ * Automatically caps gas and blob field limits based on checkpoint-level budgets and prior blocks.
63
71
  */
64
72
  async buildBlock(
65
73
  pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
66
74
  blockNumber: BlockNumber,
67
75
  timestamp: bigint,
68
- opts: PublicProcessorLimits & { expectedEndState?: StateReference },
76
+ opts: PublicProcessorLimits & { expectedEndState?: StateReference } = {},
69
77
  ): Promise<BuildBlockInCheckpointResult> {
70
- const blockBuildingTimer = new Timer();
71
78
  const slot = this.checkpointBuilder.constants.slotNumber;
72
79
 
73
- log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, { slot, blockNumber, ...opts });
80
+ this.log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, {
81
+ slot,
82
+ blockNumber,
83
+ ...opts,
84
+ currentTime: new Date(this.dateProvider.now()),
85
+ });
74
86
 
75
87
  const constants = this.checkpointBuilder.constants;
76
88
  const globalVariables = GlobalVariables.from({
@@ -85,36 +97,47 @@ export class CheckpointBuilder {
85
97
  });
86
98
  const { processor, validator } = await this.makeBlockBuilderDeps(globalVariables, this.fork);
87
99
 
100
+ // Cap gas limits amd available blob fields by remaining checkpoint-level budgets
101
+ const cappedOpts: PublicProcessorLimits & { expectedEndState?: StateReference } = {
102
+ ...opts,
103
+ ...this.capLimitsByCheckpointBudgets(opts),
104
+ };
105
+
88
106
  const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
89
- processor.process(pendingTxs, opts, validator),
107
+ processor.process(pendingTxs, cappedOpts, validator),
90
108
  );
91
109
 
110
+ // Throw if we didn't collect a single valid tx and we're not allowed to build empty blocks
111
+ // (only the first block in a checkpoint can be empty)
112
+ if (processedTxs.length === 0 && this.checkpointBuilder.getBlockCount() > 0) {
113
+ throw new NoValidTxsError(failedTxs);
114
+ }
115
+
92
116
  // Add block to checkpoint
93
- const block = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, {
117
+ const { block } = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, {
94
118
  expectedEndState: opts.expectedEndState,
95
119
  });
96
120
 
97
- // How much public gas was processed
98
- const publicGas = processedTxs.reduce((acc, tx) => acc.add(tx.gasUsed.publicGas), Gas.empty());
121
+ this.log.debug('Built block within checkpoint', {
122
+ header: block.header.toInspect(),
123
+ processedTxs: processedTxs.map(tx => tx.hash.toString()),
124
+ failedTxs: failedTxs.map(tx => tx.tx.txHash.toString()),
125
+ });
99
126
 
100
- const res = {
127
+ return {
101
128
  block,
102
- publicGas,
103
129
  publicProcessorDuration,
104
130
  numTxs: processedTxs.length,
105
131
  failedTxs,
106
- blockBuildingTimer,
107
132
  usedTxs,
108
133
  };
109
- log.debug('Built block within checkpoint', res.block.header);
110
- return res;
111
134
  }
112
135
 
113
136
  /** Completes the checkpoint and returns it. */
114
137
  async completeCheckpoint(): Promise<Checkpoint> {
115
138
  const checkpoint = await this.checkpointBuilder.completeCheckpoint();
116
139
 
117
- log.verbose(`Completed checkpoint ${checkpoint.number}`, {
140
+ this.log.verbose(`Completed checkpoint ${checkpoint.number}`, {
118
141
  checkpointNumber: checkpoint.number,
119
142
  numBlocks: checkpoint.blocks.length,
120
143
  archiveRoot: checkpoint.archive.root.toString(),
@@ -128,16 +151,79 @@ export class CheckpointBuilder {
128
151
  return this.checkpointBuilder.clone().completeCheckpoint();
129
152
  }
130
153
 
154
+ /**
155
+ * Caps per-block gas and blob field limits by remaining checkpoint-level budgets.
156
+ * Computes remaining L2 gas (mana), DA gas, and blob fields from blocks already added to the checkpoint,
157
+ * then returns opts with maxBlockGas and maxBlobFields capped accordingly.
158
+ */
159
+ protected capLimitsByCheckpointBudgets(
160
+ opts: PublicProcessorLimits,
161
+ ): Pick<PublicProcessorLimits, 'maxBlockGas' | 'maxBlobFields' | 'maxTransactions'> {
162
+ const existingBlocks = this.checkpointBuilder.getBlocks();
163
+
164
+ // Remaining L2 gas (mana)
165
+ // IMPORTANT: This assumes mana is computed solely based on L2 gas used in transactions.
166
+ // This may change in the future.
167
+ const usedMana = sum(existingBlocks.map(b => b.header.totalManaUsed.toNumber()));
168
+ const remainingMana = this.config.rollupManaLimit - usedMana;
169
+
170
+ // Remaining DA gas
171
+ const usedDAGas = sum(existingBlocks.map(b => b.computeDAGasUsed())) ?? 0;
172
+ const remainingDAGas = MAX_PROCESSABLE_DA_GAS_PER_CHECKPOINT - usedDAGas;
173
+
174
+ // Remaining blob fields (block blob fields include both tx data and block-end overhead)
175
+ const usedBlobFields = sum(existingBlocks.map(b => b.toBlobFields().length));
176
+ const totalBlobCapacity = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
177
+ const isFirstBlock = existingBlocks.length === 0;
178
+ const blockEndOverhead = getNumBlockEndBlobFields(isFirstBlock);
179
+ const maxBlobFieldsForTxs = totalBlobCapacity - usedBlobFields - blockEndOverhead;
180
+
181
+ // Cap L2 gas by remaining checkpoint mana
182
+ const cappedL2Gas = Math.min(opts.maxBlockGas?.l2Gas ?? remainingMana, remainingMana);
183
+
184
+ // Cap DA gas by remaining checkpoint DA gas budget
185
+ const cappedDAGas = Math.min(opts.maxBlockGas?.daGas ?? remainingDAGas, remainingDAGas);
186
+
187
+ // Cap blob fields by remaining checkpoint blob capacity
188
+ const cappedBlobFields =
189
+ opts.maxBlobFields !== undefined ? Math.min(opts.maxBlobFields, maxBlobFieldsForTxs) : maxBlobFieldsForTxs;
190
+
191
+ // Cap transaction count by remaining checkpoint tx budget
192
+ let cappedMaxTransactions: number | undefined;
193
+ if (this.config.maxTxsPerCheckpoint !== undefined) {
194
+ const usedTxs = sum(existingBlocks.map(b => b.body.txEffects.length));
195
+ const remainingTxs = Math.max(0, this.config.maxTxsPerCheckpoint - usedTxs);
196
+ cappedMaxTransactions =
197
+ opts.maxTransactions !== undefined ? Math.min(opts.maxTransactions, remainingTxs) : remainingTxs;
198
+ } else {
199
+ cappedMaxTransactions = opts.maxTransactions;
200
+ }
201
+
202
+ return {
203
+ maxBlockGas: new Gas(cappedDAGas, cappedL2Gas),
204
+ maxBlobFields: cappedBlobFields,
205
+ maxTransactions: cappedMaxTransactions,
206
+ };
207
+ }
208
+
131
209
  protected async makeBlockBuilderDeps(globalVariables: GlobalVariables, fork: MerkleTreeWriteOperations) {
132
- const txPublicSetupAllowList = this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
133
- const contractsDB = new PublicContractsDB(this.contractDataSource);
210
+ const txPublicSetupAllowList = [
211
+ ...(await getDefaultAllowedSetupFunctions()),
212
+ ...(this.config.txPublicSetupAllowListExtend ?? []),
213
+ ];
214
+ const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
134
215
  const guardedFork = new GuardedMerkleTreeOperations(fork);
135
216
 
217
+ const collectDebugLogs = this.debugLogStore.isEnabled;
218
+
219
+ const bindings = this.log.getBindings();
136
220
  const publicTxSimulator = createPublicTxSimulatorForBlockBuilding(
137
221
  guardedFork,
138
222
  contractsDB,
139
223
  globalVariables,
140
224
  this.telemetryClient,
225
+ bindings,
226
+ collectDebugLogs,
141
227
  );
142
228
 
143
229
  const processor = new PublicProcessor(
@@ -147,15 +233,17 @@ export class CheckpointBuilder {
147
233
  publicTxSimulator,
148
234
  this.dateProvider,
149
235
  this.telemetryClient,
150
- undefined,
236
+ createLogger('simulator:public-processor', bindings),
151
237
  this.config,
238
+ this.debugLogStore,
152
239
  );
153
240
 
154
- const validator = createValidatorForBlockBuilding(
241
+ const validator = createTxValidatorForBlockBuilding(
155
242
  fork,
156
243
  this.contractDataSource,
157
244
  globalVariables,
158
245
  txPublicSetupAllowList,
246
+ this.log.getBindings(),
159
247
  );
160
248
 
161
249
  return {
@@ -165,16 +253,20 @@ export class CheckpointBuilder {
165
253
  }
166
254
  }
167
255
 
168
- /**
169
- * Factory for creating checkpoint builders.
170
- */
171
- export class FullNodeCheckpointsBuilder {
256
+ /** Factory for creating checkpoint builders. */
257
+ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
258
+ private log: Logger;
259
+
172
260
  constructor(
173
- private config: FullNodeBlockBuilderConfig,
261
+ private config: FullNodeBlockBuilderConfig & Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>,
262
+ private worldState: WorldStateSynchronizer,
174
263
  private contractDataSource: ContractDataSource,
175
264
  private dateProvider: DateProvider,
176
265
  private telemetryClient: TelemetryClient = getTelemetryClient(),
177
- ) {}
266
+ private debugLogStore: DebugLogStore = new NullDebugLogStore(),
267
+ ) {
268
+ this.log = createLogger('checkpoint-builder');
269
+ }
178
270
 
179
271
  public getConfig(): FullNodeBlockBuilderConfig {
180
272
  return this.config;
@@ -190,25 +282,32 @@ export class FullNodeCheckpointsBuilder {
190
282
  async startCheckpoint(
191
283
  checkpointNumber: CheckpointNumber,
192
284
  constants: CheckpointGlobalVariables,
285
+ feeAssetPriceModifier: bigint,
193
286
  l1ToL2Messages: Fr[],
287
+ previousCheckpointOutHashes: Fr[],
194
288
  fork: MerkleTreeWriteOperations,
289
+ bindings?: LoggerBindings,
195
290
  ): Promise<CheckpointBuilder> {
196
291
  const stateReference = await fork.getStateReference();
197
292
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
198
293
 
199
- log.verbose(`Building new checkpoint ${checkpointNumber}`, {
294
+ this.log.verbose(`Building new checkpoint ${checkpointNumber}`, {
200
295
  checkpointNumber,
201
296
  msgCount: l1ToL2Messages.length,
202
297
  initialStateReference: stateReference.toInspect(),
203
298
  initialArchiveRoot: bufferToHex(archiveTree.root),
204
299
  constants,
300
+ feeAssetPriceModifier,
205
301
  });
206
302
 
207
303
  const lightweightBuilder = await LightweightCheckpointBuilder.startNewCheckpoint(
208
304
  checkpointNumber,
209
305
  constants,
210
306
  l1ToL2Messages,
307
+ previousCheckpointOutHashes,
211
308
  fork,
309
+ bindings,
310
+ feeAssetPriceModifier,
212
311
  );
213
312
 
214
313
  return new CheckpointBuilder(
@@ -218,6 +317,8 @@ export class FullNodeCheckpointsBuilder {
218
317
  this.contractDataSource,
219
318
  this.dateProvider,
220
319
  this.telemetryClient,
320
+ bindings,
321
+ this.debugLogStore,
221
322
  );
222
323
  }
223
324
 
@@ -227,32 +328,47 @@ export class FullNodeCheckpointsBuilder {
227
328
  async openCheckpoint(
228
329
  checkpointNumber: CheckpointNumber,
229
330
  constants: CheckpointGlobalVariables,
331
+ feeAssetPriceModifier: bigint,
230
332
  l1ToL2Messages: Fr[],
333
+ previousCheckpointOutHashes: Fr[],
231
334
  fork: MerkleTreeWriteOperations,
232
- existingBlocks: L2BlockNew[] = [],
335
+ existingBlocks: L2Block[] = [],
336
+ bindings?: LoggerBindings,
233
337
  ): Promise<CheckpointBuilder> {
234
338
  const stateReference = await fork.getStateReference();
235
339
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
236
340
 
237
341
  if (existingBlocks.length === 0) {
238
- return this.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork);
342
+ return this.startCheckpoint(
343
+ checkpointNumber,
344
+ constants,
345
+ feeAssetPriceModifier,
346
+ l1ToL2Messages,
347
+ previousCheckpointOutHashes,
348
+ fork,
349
+ bindings,
350
+ );
239
351
  }
240
352
 
241
- log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
353
+ this.log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
242
354
  checkpointNumber,
243
355
  msgCount: l1ToL2Messages.length,
244
356
  existingBlockCount: existingBlocks.length,
245
357
  initialStateReference: stateReference.toInspect(),
246
358
  initialArchiveRoot: bufferToHex(archiveTree.root),
247
359
  constants,
360
+ feeAssetPriceModifier,
248
361
  });
249
362
 
250
363
  const lightweightBuilder = await LightweightCheckpointBuilder.resumeCheckpoint(
251
364
  checkpointNumber,
252
365
  constants,
366
+ feeAssetPriceModifier,
253
367
  l1ToL2Messages,
368
+ previousCheckpointOutHashes,
254
369
  fork,
255
370
  existingBlocks,
371
+ bindings,
256
372
  );
257
373
 
258
374
  return new CheckpointBuilder(
@@ -262,6 +378,13 @@ export class FullNodeCheckpointsBuilder {
262
378
  this.contractDataSource,
263
379
  this.dateProvider,
264
380
  this.telemetryClient,
381
+ bindings,
382
+ this.debugLogStore,
265
383
  );
266
384
  }
385
+
386
+ /** Returns a fork of the world state at the given block number. */
387
+ getFork(blockNumber: BlockNumber): Promise<MerkleTreeWriteOperations> {
388
+ return this.worldState.fork(blockNumber);
389
+ }
267
390
  }
package/src/config.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  secretValueConfigHelper,
7
7
  } from '@aztec/foundation/config';
8
8
  import { EthAddress } from '@aztec/foundation/eth-address';
9
+ import { localSignerConfigMappings, validatorHASignerConfigMappings } from '@aztec/stdlib/ha-signing';
9
10
  import type { ValidatorClientConfig } from '@aztec/stdlib/interfaces/server';
10
11
 
11
12
  export type { ValidatorClientConfig };
@@ -53,16 +54,10 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
53
54
  description: 'Re-execute transactions before attesting',
54
55
  ...booleanConfigHelper(true),
55
56
  },
56
- validatorReexecuteDeadlineMs: {
57
- env: 'VALIDATOR_REEXECUTE_DEADLINE_MS',
58
- description: 'Will re-execute until this many milliseconds are left in the slot',
59
- ...numberConfigHelper(6000),
60
- },
61
57
  alwaysReexecuteBlockProposals: {
62
- env: 'ALWAYS_REEXECUTE_BLOCK_PROPOSALS',
63
58
  description:
64
59
  'Whether to always reexecute block proposals, even for non-validator nodes (useful for monitoring network status).',
65
- ...booleanConfigHelper(false),
60
+ defaultValue: true,
66
61
  },
67
62
  fishermanMode: {
68
63
  env: 'FISHERMAN_MODE',
@@ -70,16 +65,40 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
70
65
  'Whether to run in fisherman mode: validates all proposals and attestations but does not broadcast attestations or participate in consensus.',
71
66
  ...booleanConfigHelper(false),
72
67
  },
73
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable
74
68
  skipCheckpointProposalValidation: {
75
- description: 'Skip checkpoint proposal validation and always attest (default: true)',
76
- defaultValue: true,
69
+ description: 'Skip checkpoint proposal validation and always attest (default: false)',
70
+ defaultValue: false,
77
71
  },
78
- // TODO(palla/mbps): Change default to false once block sync is stable
79
72
  skipPushProposedBlocksToArchiver: {
80
- description: 'Skip pushing re-executed blocks to archiver (default: true)',
81
- defaultValue: true,
73
+ description: 'Skip pushing re-executed blocks to archiver (default: false)',
74
+ defaultValue: false,
75
+ },
76
+ attestToEquivocatedProposals: {
77
+ description: 'Agree to attest to equivocated checkpoint proposals (for testing purposes only)',
78
+ ...booleanConfigHelper(false),
79
+ },
80
+ validateMaxL2BlockGas: {
81
+ env: 'VALIDATOR_MAX_L2_BLOCK_GAS',
82
+ description: 'Maximum L2 block gas for validation. Proposals exceeding this limit are rejected.',
83
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
84
+ },
85
+ validateMaxDABlockGas: {
86
+ env: 'VALIDATOR_MAX_DA_BLOCK_GAS',
87
+ description: 'Maximum DA block gas for validation. Proposals exceeding this limit are rejected.',
88
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
89
+ },
90
+ validateMaxTxsPerBlock: {
91
+ env: 'VALIDATOR_MAX_TX_PER_BLOCK',
92
+ description: 'Maximum transactions per block for validation. Proposals exceeding this limit are rejected.',
93
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
94
+ },
95
+ validateMaxTxsPerCheckpoint: {
96
+ env: 'VALIDATOR_MAX_TX_PER_CHECKPOINT',
97
+ description: 'Maximum transactions per checkpoint for validation. Proposals exceeding this limit are rejected.',
98
+ parseEnv: (val: string) => (val ? parseInt(val, 10) : undefined),
82
99
  },
100
+ ...localSignerConfigMappings,
101
+ ...validatorHASignerConfigMappings,
83
102
  };
84
103
 
85
104
  /**
@@ -1,3 +1,9 @@
1
+ import {
2
+ BlockNumber,
3
+ type CheckpointNumber,
4
+ IndexWithinCheckpoint,
5
+ type SlotNumber,
6
+ } from '@aztec/foundation/branded-types';
1
7
  import { Buffer32 } from '@aztec/foundation/buffer';
2
8
  import { keccak256 } from '@aztec/foundation/crypto/keccak';
3
9
  import { Fr } from '@aztec/foundation/curves/bn254';
@@ -18,6 +24,8 @@ import {
18
24
  } from '@aztec/stdlib/p2p';
19
25
  import type { CheckpointHeader } from '@aztec/stdlib/rollup';
20
26
  import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
27
+ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validator-ha-signer/errors';
28
+ import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
21
29
 
22
30
  import type { ValidatorKeyStore } from '../key_store/interface.js';
23
31
 
@@ -31,34 +39,40 @@ export class ValidationService {
31
39
  * Create a block proposal with the given header, archive, and transactions
32
40
  *
33
41
  * @param blockHeader - The block header
34
- * @param indexWithinCheckpoint - Index of this block within the checkpoint (0-indexed)
42
+ * @param blockIndexWithinCheckpoint - The block index within checkpoint for HA signing context
35
43
  * @param inHash - Hash of L1 to L2 messages for this checkpoint
36
44
  * @param archive - The archive of the current block
37
- * @param txs - TxHash[] ordered list of transactions
45
+ * @param txs - Ordered list of transactions (Tx[])
46
+ * @param proposerAttesterAddress - The address of the proposer/attester, or undefined
38
47
  * @param options - Block proposal options (including broadcastInvalidBlockProposal for testing)
39
48
  *
40
49
  * @returns A block proposal signing the above information
50
+ * @throws DutyAlreadySignedError if HA signer indicates duty already signed by another node
51
+ * @throws SlashingProtectionError if attempting to sign different data for same slot
41
52
  */
42
53
  public createBlockProposal(
43
54
  blockHeader: BlockHeader,
44
- indexWithinCheckpoint: number,
55
+ blockIndexWithinCheckpoint: IndexWithinCheckpoint,
45
56
  inHash: Fr,
46
57
  archive: Fr,
47
58
  txs: Tx[],
48
59
  proposerAttesterAddress: EthAddress | undefined,
49
60
  options: BlockProposalOptions,
50
61
  ): Promise<BlockProposal> {
51
- const payloadSigner = this.getPayloadSigner(proposerAttesterAddress);
52
-
53
62
  // For testing: change the new archive to trigger state_mismatch validation failure
54
63
  if (options.broadcastInvalidBlockProposal) {
55
64
  archive = Fr.random();
56
65
  this.log.warn(`Creating INVALID block proposal for slot ${blockHeader.globalVariables.slotNumber}`);
57
66
  }
58
67
 
68
+ // Create a signer that uses the appropriate address
69
+ const address = proposerAttesterAddress ?? this.keyStore.getAddress(0);
70
+ const payloadSigner = (payload: Buffer32, context: SigningContext) =>
71
+ this.keyStore.signMessageWithAddress(address, payload, context);
72
+
59
73
  return BlockProposal.createProposalFromSigner(
60
74
  blockHeader,
61
- indexWithinCheckpoint,
75
+ blockIndexWithinCheckpoint,
62
76
  inHash,
63
77
  archive,
64
78
  txs.map(tx => tx.getTxHash()),
@@ -81,18 +95,23 @@ export class ValidationService {
81
95
  public createCheckpointProposal(
82
96
  checkpointHeader: CheckpointHeader,
83
97
  archive: Fr,
98
+ feeAssetPriceModifier: bigint,
84
99
  lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
85
100
  proposerAttesterAddress: EthAddress | undefined,
86
101
  options: CheckpointProposalOptions,
87
102
  ): Promise<CheckpointProposal> {
88
- const payloadSigner = this.getPayloadSigner(proposerAttesterAddress);
89
-
90
103
  // For testing: change the archive to trigger state_mismatch validation failure
91
104
  if (options.broadcastInvalidCheckpointProposal) {
92
105
  archive = Fr.random();
93
106
  this.log.warn(`Creating INVALID checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
94
107
  }
95
108
 
109
+ // Create a signer that takes payload and context, and uses the appropriate address
110
+ const payloadSigner = (payload: Buffer32, context: SigningContext) => {
111
+ const address = proposerAttesterAddress ?? this.keyStore.getAddress(0);
112
+ return this.keyStore.signMessageWithAddress(address, payload, context);
113
+ };
114
+
96
115
  // Last block to include in the proposal
97
116
  const lastBlock = lastBlockInfo && {
98
117
  blockHeader: lastBlockInfo.blockHeader,
@@ -101,17 +120,13 @@ export class ValidationService {
101
120
  txs: options.publishFullTxs ? lastBlockInfo.txs : undefined,
102
121
  };
103
122
 
104
- return CheckpointProposal.createProposalFromSigner(checkpointHeader, archive, lastBlock, payloadSigner);
105
- }
106
-
107
- private getPayloadSigner(proposerAttesterAddress: EthAddress | undefined): (payload: Buffer32) => Promise<Signature> {
108
- if (proposerAttesterAddress !== undefined) {
109
- return (payload: Buffer32) => this.keyStore.signMessageWithAddress(proposerAttesterAddress, payload);
110
- } else {
111
- // if there is no proposer attester address, just use the first signer
112
- const signer = this.keyStore.getAddress(0);
113
- return (payload: Buffer32) => this.keyStore.signMessageWithAddress(signer, payload);
114
- }
123
+ return CheckpointProposal.createProposalFromSigner(
124
+ checkpointHeader,
125
+ archive,
126
+ feeAssetPriceModifier,
127
+ lastBlock,
128
+ payloadSigner,
129
+ );
115
130
  }
116
131
 
117
132
  /**
@@ -129,23 +144,77 @@ export class ValidationService {
129
144
  attestors: EthAddress[],
130
145
  ): Promise<CheckpointAttestation[]> {
131
146
  // Create the attestation payload from the checkpoint proposal
132
- const payload = new ConsensusPayload(proposal.checkpointHeader, proposal.archive);
147
+ const payload = new ConsensusPayload(proposal.checkpointHeader, proposal.archive, proposal.feeAssetPriceModifier);
133
148
  const buf = Buffer32.fromBuffer(
134
149
  keccak256(payload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation)),
135
150
  );
136
- const signatures = await Promise.all(
137
- attestors.map(attestor => this.keyStore.signMessageWithAddress(attestor, buf)),
151
+
152
+ // TODO(spy/ha): Use checkpointNumber instead of blockNumber once CheckpointHeader includes it.
153
+ // CheckpointProposalCore doesn't have lastBlock info, so use 0 as a proxy.
154
+ // blockNumber is NOT used for the primary key so it's safe to use here.
155
+ // See CheckpointHeader TODO and SigningContext types documentation.
156
+ const blockNumber = BlockNumber(0);
157
+ const context: SigningContext = {
158
+ slot: proposal.slotNumber,
159
+ blockNumber,
160
+ dutyType: DutyType.ATTESTATION,
161
+ };
162
+
163
+ // Sign each attestor in parallel, catching HA errors per-attestor
164
+ const results = await Promise.allSettled(
165
+ attestors.map(async attestor => {
166
+ const sig = await this.keyStore.signMessageWithAddress(attestor, buf, context);
167
+ // return new BlockAttestation(proposal.payload, sig, proposal.signature);
168
+ return new CheckpointAttestation(payload, sig, proposal.signature);
169
+ }),
138
170
  );
139
- return signatures.map(sig => new CheckpointAttestation(payload, sig, proposal.signature));
171
+
172
+ const attestations: CheckpointAttestation[] = [];
173
+ for (let i = 0; i < results.length; i++) {
174
+ const result = results[i];
175
+ if (result.status === 'fulfilled') {
176
+ attestations.push(result.value);
177
+ } else {
178
+ const error = result.reason;
179
+ if (error instanceof DutyAlreadySignedError || error instanceof SlashingProtectionError) {
180
+ this.log.info(
181
+ `Attestation for slot ${proposal.slotNumber} by ${attestors[i]} already signed by another High-Availability node`,
182
+ );
183
+ // Continue with remaining attestors
184
+ } else {
185
+ throw error;
186
+ }
187
+ }
188
+ }
189
+
190
+ return attestations;
140
191
  }
141
192
 
142
- async signAttestationsAndSigners(
193
+ /**
194
+ * Sign attestations and signers payload
195
+ * @param attestationsAndSigners - The attestations and signers to sign
196
+ * @param proposer - The proposer address to sign with
197
+ * @param slot - The slot number for HA signing context
198
+ * @param blockNumber - The block or checkpoint number for HA signing context
199
+ * @returns signature
200
+ * @throws DutyAlreadySignedError if already signed by another HA node
201
+ * @throws SlashingProtectionError if attempting to sign different data for same slot
202
+ */
203
+ signAttestationsAndSigners(
143
204
  attestationsAndSigners: CommitteeAttestationsAndSigners,
144
205
  proposer: EthAddress,
206
+ slot: SlotNumber,
207
+ blockNumber: BlockNumber | CheckpointNumber,
145
208
  ): Promise<Signature> {
209
+ const context: SigningContext = {
210
+ slot,
211
+ blockNumber,
212
+ dutyType: DutyType.ATTESTATIONS_AND_SIGNERS,
213
+ };
214
+
146
215
  const buf = Buffer32.fromBuffer(
147
216
  keccak256(attestationsAndSigners.getPayloadToSign(SignatureDomainSeparator.attestationsAndSigners)),
148
217
  );
149
- return await this.keyStore.signMessageWithAddress(proposer, buf);
218
+ return this.keyStore.signMessageWithAddress(proposer, buf, context);
150
219
  }
151
220
  }