@aztec/validator-client 0.0.1-commit.2f68f620 → 0.0.1-commit.3100065

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.
package/dest/validator.js CHANGED
@@ -8,49 +8,23 @@ import { DateProvider } from '@aztec/foundation/timer';
8
8
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
9
9
  import { OffenseType, WANT_TO_CLEAR_SLASH_EVENT, WANT_TO_SLASH_EVENT, getOffenseTypeName } from '@aztec/slasher';
10
10
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
11
+ import { ConsensusTimetable } from '@aztec/stdlib/timetable';
11
12
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
12
13
  import { getTelemetryClient } from '@aztec/telemetry-client';
13
14
  import { createHASigner, createLocalSignerWithProtection, createSignerFromSharedDb } from '@aztec/validator-ha-signer/factory';
14
15
  import { DutyType } from '@aztec/validator-ha-signer/types';
15
16
  import { EventEmitter } from 'events';
17
+ import { DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS } from './config.js';
16
18
  import { ValidationService } from './duties/validation_service.js';
17
19
  import { HAKeyStore } from './key_store/ha_key_store.js';
18
20
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
19
21
  import { ValidatorMetrics } from './metrics.js';
20
- import { ProposalHandler } from './proposal_handler.js';
22
+ import { ProposalHandler, SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT, SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT } from './proposal_handler.js';
21
23
  // We maintain a set of proposers who have proposed invalid blocks.
22
24
  // Just cap the set to avoid unbounded growth.
23
25
  const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
24
- const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
25
26
  const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
26
27
  const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
27
- // What errors from the block proposal handler result in slashing
28
- const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
29
- 'state_mismatch',
30
- 'failed_txs',
31
- 'global_variables_mismatch',
32
- 'invalid_proposal',
33
- 'parent_block_wrong_slot',
34
- 'in_hash_mismatch'
35
- ];
36
- const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
37
- // enabled
38
- ['invalid_fee_asset_price_modifier']: true,
39
- ['checkpoint_header_mismatch']: true,
40
- // These late mismatches should normally be caught by earlier checks, but if reached after validating the local
41
- // checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
42
- ['archive_mismatch']: true,
43
- ['out_hash_mismatch']: true,
44
- ['no_blocks_for_slot']: true,
45
- ['too_many_blocks_in_checkpoint']: true,
46
- ['checkpoint_validation_failed']: true,
47
- ['last_block_archive_mismatch']: true,
48
- // disabled
49
- ['invalid_signature']: false,
50
- ['last_block_not_found']: false,
51
- ['block_fetch_error']: false,
52
- ['checkpoint_already_published']: false
53
- };
54
28
  /**
55
29
  * Validator Client
56
30
  */ export class ValidatorClient extends EventEmitter {
@@ -78,13 +52,12 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
78
52
  epochCacheUpdateLoop;
79
53
  /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
80
54
  proposersOfInvalidBlocks;
81
- slotsWithInvalidProposals;
82
55
  invalidCheckpointProposalOffenseKeys;
56
+ oversizedProposalOffenseKeys;
83
57
  badAttestationOffenseKeys;
84
- slotsWithProposalEquivocation;
85
58
  /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
86
59
  constructor(keyStore, epochCache, p2pClient, proposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
87
- super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.proposalHandler = proposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.slashingProtectionSigner = slashingProtectionSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = FifoSet.withLimit(MAX_PROPOSERS_OF_INVALID_BLOCKS), this.slotsWithInvalidProposals = FifoSet.withLimit(MAX_TRACKED_INVALID_PROPOSAL_SLOTS), this.invalidCheckpointProposalOffenseKeys = FifoSet.withLimit(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS), this.badAttestationOffenseKeys = FifoSet.withLimit(MAX_TRACKED_BAD_ATTESTATIONS), this.slotsWithProposalEquivocation = FifoSet.withLimit(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
60
+ super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.proposalHandler = proposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.slashingProtectionSigner = slashingProtectionSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = FifoSet.withLimit(MAX_PROPOSERS_OF_INVALID_BLOCKS), this.invalidCheckpointProposalOffenseKeys = FifoSet.withLimit(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS), this.oversizedProposalOffenseKeys = FifoSet.withLimit(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS), this.badAttestationOffenseKeys = FifoSet.withLimit(MAX_TRACKED_BAD_ATTESTATIONS);
88
61
  // Create child logger with fisherman prefix if in fisherman mode
89
62
  this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
90
63
  this.tracer = telemetry.getTracer('Validator');
@@ -142,7 +115,11 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
142
115
  }
143
116
  static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, reexecutionTracker, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), slashingProtectionDb) {
144
117
  const metrics = new ValidatorMetrics(telemetry);
145
- const blockProposalValidator = new BlockProposalValidator(epochCache, {
118
+ const consensusTimetable = new ConsensusTimetable({
119
+ l1Constants: epochCache.getL1Constants(),
120
+ blockDuration: config.blockDurationMs / 1000
121
+ });
122
+ const blockProposalValidator = new BlockProposalValidator(epochCache, consensusTimetable, {
146
123
  txsPermitted: !config.disableTransactions,
147
124
  maxTxsPerBlock: config.validateMaxTxsPerBlock,
148
125
  maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
@@ -150,9 +127,10 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
150
127
  signatureContext: {
151
128
  chainId: config.l1ChainId,
152
129
  rollupAddress: config.rollupAddress
153
- }
130
+ },
131
+ clockDisparityMs: config.maxGossipClockDisparityMs ?? DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS
154
132
  });
155
- const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, reexecutionTracker, metrics, dateProvider, telemetry, undefined);
133
+ const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, consensusTimetable, config, blobClient, reexecutionTracker, metrics, dateProvider, telemetry, undefined);
156
134
  const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
157
135
  let slashingProtectionSigner;
158
136
  if (slashingProtectionDb) {
@@ -209,10 +187,10 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
209
187
  return this.config;
210
188
  }
211
189
  hasProposalEquivocation(slotNumber) {
212
- return this.slotsWithProposalEquivocation.has(slotNumber);
190
+ return this.proposalHandler.hasProposalEquivocation(slotNumber);
213
191
  }
214
192
  hasInvalidProposals(slotNumber) {
215
- return this.slotsWithInvalidProposals.has(slotNumber);
193
+ return this.proposalHandler.hasInvalidProposals(slotNumber);
216
194
  }
217
195
  updateConfig(config) {
218
196
  this.config = {
@@ -262,6 +240,10 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
262
240
  this.p2pClient.registerDuplicateProposalCallback((info)=>{
263
241
  this.handleDuplicateProposal(info);
264
242
  });
243
+ // Oversized proposal handler - triggers slashing for proposals beyond the per-checkpoint block limit
244
+ this.p2pClient.registerOversizedProposalCallback((info)=>{
245
+ this.handleOversizedProposal(info);
246
+ });
265
247
  // Duplicate attestation handler - triggers slashing for attestation equivocation
266
248
  this.p2pClient.registerDuplicateAttestationCallback((info)=>{
267
249
  this.handleDuplicateAttestation(info);
@@ -369,7 +351,7 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
369
351
  }
370
352
  // Ignore proposals from ourselves (may happen in HA setups)
371
353
  if (proposer && this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
372
- this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
354
+ this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
373
355
  proposer: proposer.toString(),
374
356
  proposalSlotNumber
375
357
  });
@@ -522,7 +504,8 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
522
504
  if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
523
505
  return;
524
506
  }
525
- this.markInvalidProposalSlot(proposal.slotNumber);
507
+ // The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
508
+ // so we only emit the proposer slash event here.
526
509
  if (this.slashInvalidCheckpointProposal(proposal)) {
527
510
  this.log.info(`Detected invalid checkpoint proposal offense`, {
528
511
  ...proposalInfo,
@@ -557,11 +540,11 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
557
540
  return true;
558
541
  }
559
542
  markInvalidProposalSlot(slotNumber) {
560
- this.slotsWithInvalidProposals.add(slotNumber);
543
+ this.proposalHandler.markInvalidProposalSlot(slotNumber);
561
544
  }
562
545
  handleCheckpointAttestation(attestation) {
563
546
  const slotNumber = attestation.slotNumber;
564
- if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
547
+ if (!this.proposalHandler.hasInvalidProposals(slotNumber) || this.proposalHandler.hasProposalEquivocation(slotNumber)) {
565
548
  return;
566
549
  }
567
550
  const attester = attestation.getSender();
@@ -595,11 +578,37 @@ const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
595
578
  ]);
596
579
  }
597
580
  /**
581
+ * Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
582
+ * beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
583
+ * self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
584
+ * (proposer, slot) since the p2p layer reports every oversized proposal it stores.
585
+ */ handleOversizedProposal(info) {
586
+ const { slot, proposer } = info;
587
+ const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
588
+ if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
589
+ return;
590
+ }
591
+ this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
592
+ proposer: proposer.toString(),
593
+ slot,
594
+ amount: this.config.slashBroadcastedInvalidBlockPenalty,
595
+ offenseType: getOffenseTypeName(offenseType)
596
+ });
597
+ this.emit(WANT_TO_SLASH_EVENT, [
598
+ {
599
+ validator: proposer,
600
+ amount: this.config.slashBroadcastedInvalidBlockPenalty,
601
+ offenseType,
602
+ epochOrSlot: BigInt(slot)
603
+ }
604
+ ]);
605
+ }
606
+ /**
598
607
  * Handle detection of a duplicate proposal (equivocation).
599
608
  * Emits a slash event when a proposer sends multiple proposals for the same position.
600
609
  */ handleDuplicateProposal(info) {
601
610
  const { slot, proposer, type } = info;
602
- this.slotsWithProposalEquivocation.add(slot);
611
+ this.proposalHandler.markProposalEquivocation(slot);
603
612
  this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
604
613
  proposer: proposer.toString(),
605
614
  slot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.2f68f620",
3
+ "version": "0.0.1-commit.3100065",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,30 +64,30 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/blob-client": "0.0.1-commit.2f68f620",
68
- "@aztec/blob-lib": "0.0.1-commit.2f68f620",
69
- "@aztec/constants": "0.0.1-commit.2f68f620",
70
- "@aztec/epoch-cache": "0.0.1-commit.2f68f620",
71
- "@aztec/ethereum": "0.0.1-commit.2f68f620",
72
- "@aztec/foundation": "0.0.1-commit.2f68f620",
73
- "@aztec/node-keystore": "0.0.1-commit.2f68f620",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.2f68f620",
75
- "@aztec/p2p": "0.0.1-commit.2f68f620",
76
- "@aztec/protocol-contracts": "0.0.1-commit.2f68f620",
77
- "@aztec/prover-client": "0.0.1-commit.2f68f620",
78
- "@aztec/simulator": "0.0.1-commit.2f68f620",
79
- "@aztec/slasher": "0.0.1-commit.2f68f620",
80
- "@aztec/stdlib": "0.0.1-commit.2f68f620",
81
- "@aztec/telemetry-client": "0.0.1-commit.2f68f620",
82
- "@aztec/validator-ha-signer": "0.0.1-commit.2f68f620",
67
+ "@aztec/blob-client": "0.0.1-commit.3100065",
68
+ "@aztec/blob-lib": "0.0.1-commit.3100065",
69
+ "@aztec/constants": "0.0.1-commit.3100065",
70
+ "@aztec/epoch-cache": "0.0.1-commit.3100065",
71
+ "@aztec/ethereum": "0.0.1-commit.3100065",
72
+ "@aztec/foundation": "0.0.1-commit.3100065",
73
+ "@aztec/node-keystore": "0.0.1-commit.3100065",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.3100065",
75
+ "@aztec/p2p": "0.0.1-commit.3100065",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.3100065",
77
+ "@aztec/prover-client": "0.0.1-commit.3100065",
78
+ "@aztec/simulator": "0.0.1-commit.3100065",
79
+ "@aztec/slasher": "0.0.1-commit.3100065",
80
+ "@aztec/stdlib": "0.0.1-commit.3100065",
81
+ "@aztec/telemetry-client": "0.0.1-commit.3100065",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.3100065",
83
83
  "koa": "^2.16.1",
84
84
  "koa-router": "^13.1.1",
85
85
  "tslib": "^2.4.0",
86
86
  "viem": "npm:@aztec/viem@2.38.2"
87
87
  },
88
88
  "devDependencies": {
89
- "@aztec/archiver": "0.0.1-commit.2f68f620",
90
- "@aztec/world-state": "0.0.1-commit.2f68f620",
89
+ "@aztec/archiver": "0.0.1-commit.3100065",
90
+ "@aztec/world-state": "0.0.1-commit.3100065",
91
91
  "@electric-sql/pglite": "^0.3.14",
92
92
  "@jest/globals": "^30.0.0",
93
93
  "@types/jest": "^30.0.0",
@@ -219,10 +219,12 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
219
219
  if (opts.isBuildingProposal) {
220
220
  const remainingBlocks = Math.max(1, opts.maxBlocksPerCheckpoint - existingBlocks.length);
221
221
  const multiplier = opts.perBlockAllocationMultiplier;
222
+ // DA gas and blob fields use a higher multiplier so the largest contract class deploy fits a block.
223
+ const daMultiplier = opts.perBlockDAAllocationMultiplier ?? multiplier;
222
224
 
223
225
  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
+ cappedDAGas = Math.min(cappedDAGas, Math.ceil((remainingDAGas / remainingBlocks) * daMultiplier));
227
+ cappedBlobFields = Math.min(cappedBlobFields, Math.ceil((maxBlobFieldsForTxs / remainingBlocks) * daMultiplier));
226
228
  cappedMaxTransactions = Math.min(cappedMaxTransactions, Math.ceil((remainingTxs / remainingBlocks) * multiplier));
227
229
  }
228
230
 
package/src/config.ts CHANGED
@@ -4,15 +4,26 @@ import {
4
4
  getConfigFromMappings,
5
5
  numberConfigHelper,
6
6
  optionalNumberConfigHelper,
7
+ pickConfigMappings,
7
8
  secretValueConfigHelper,
8
9
  } from '@aztec/foundation/config';
9
10
  import { EthAddress } from '@aztec/foundation/eth-address';
11
+ import { type SequencerConfig, sharedSequencerConfigMappings } from '@aztec/stdlib/config';
10
12
  import { localSignerConfigMappings, validatorHASignerConfigMappings } from '@aztec/stdlib/ha-signing';
11
13
  import type { ValidatorClientConfig } from '@aztec/stdlib/interfaces/server';
12
14
 
13
15
  export type { ValidatorClientConfig };
14
16
 
15
- export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientConfig> = {
17
+ /**
18
+ * Default clock-disparity tolerance (ms) for proposal/attestation receive windows, mirroring the p2p config
19
+ * default. Used by the validator-client validators when the merged node config does not carry the value.
20
+ */
21
+ export const DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS = 500;
22
+
23
+ export const validatorClientConfigMappings: ConfigMappingsType<
24
+ ValidatorClientConfig & Pick<SequencerConfig, 'blockDurationMs'>
25
+ > = {
26
+ ...pickConfigMappings(sharedSequencerConfigMappings, ['blockDurationMs']),
16
27
  validatorPrivateKeys: {
17
28
  env: 'VALIDATOR_PRIVATE_KEYS',
18
29
  description: 'List of private keys of the validators participating in attestation duties',
@@ -112,6 +123,8 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
112
123
  * Note: If an environment variable is not set, the default value is used.
113
124
  * @returns The validator configuration.
114
125
  */
115
- export function getProverEnvVars(): ValidatorClientConfig {
116
- return getConfigFromMappings<ValidatorClientConfig>(validatorClientConfigMappings);
126
+ export function getProverEnvVars(): ValidatorClientConfig & Pick<SequencerConfig, 'blockDurationMs'> {
127
+ return getConfigFromMappings<ValidatorClientConfig & Pick<SequencerConfig, 'blockDurationMs'>>(
128
+ validatorClientConfigMappings,
129
+ );
117
130
  }
package/src/factory.ts CHANGED
@@ -7,10 +7,12 @@ import type { L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
7
7
  import type { CheckpointReexecutionTracker } from '@aztec/stdlib/checkpoint';
8
8
  import type { ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
9
9
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
10
+ import { ConsensusTimetable } from '@aztec/stdlib/timetable';
10
11
  import type { TelemetryClient } from '@aztec/telemetry-client';
11
12
  import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
12
13
 
13
14
  import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
15
+ import { DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS } from './config.js';
14
16
  import { ValidatorMetrics } from './metrics.js';
15
17
  import { ProposalHandler } from './proposal_handler.js';
16
18
  import { ValidatorClient } from './validator.js';
@@ -31,7 +33,11 @@ export function createProposalHandler(
31
33
  },
32
34
  ) {
33
35
  const metrics = new ValidatorMetrics(deps.telemetry);
34
- const blockProposalValidator = new BlockProposalValidator(deps.epochCache, {
36
+ const consensusTimetable = new ConsensusTimetable({
37
+ l1Constants: deps.epochCache.getL1Constants(),
38
+ blockDuration: config.blockDurationMs / 1000,
39
+ });
40
+ const blockProposalValidator = new BlockProposalValidator(deps.epochCache, consensusTimetable, {
35
41
  txsPermitted: !config.disableTransactions,
36
42
  maxTxsPerBlock: config.validateMaxTxsPerBlock ?? config.validateMaxTxsPerCheckpoint,
37
43
  maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
@@ -39,6 +45,7 @@ export function createProposalHandler(
39
45
  chainId: config.l1ChainId,
40
46
  rollupAddress: config.rollupAddress,
41
47
  },
48
+ clockDisparityMs: config.maxGossipClockDisparityMs ?? DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS,
42
49
  });
43
50
  return new ProposalHandler(
44
51
  deps.checkpointsBuilder,
@@ -48,6 +55,7 @@ export function createProposalHandler(
48
55
  deps.p2pClient.getTxProvider(),
49
56
  blockProposalValidator,
50
57
  deps.epochCache,
58
+ consensusTimetable,
51
59
  config,
52
60
  deps.blobClient,
53
61
  deps.reexecutionTracker,
@@ -8,6 +8,9 @@ import type { TypedDataDefinition } from 'viem';
8
8
 
9
9
  import type { ValidatorKeyStore } from './interface.js';
10
10
 
11
+ /** Default hard timeout (ms) applied to each Web3Signer HTTP request. */
12
+ const DEFAULT_WEB3SIGNER_REQUEST_TIMEOUT_MS = 30_000;
13
+
11
14
  /**
12
15
  * Web3Signer Key Store
13
16
  *
@@ -15,10 +18,15 @@ import type { ValidatorKeyStore } from './interface.js';
15
18
  * This implementation uses the Web3Signer JSON-RPC API for secp256k1 signatures.
16
19
  */
17
20
  export class Web3SignerKeyStore implements ValidatorKeyStore {
21
+ private readonly requestTimeoutMs: number;
22
+
18
23
  constructor(
19
24
  private addresses: EthAddress[],
20
25
  private baseUrl: string,
21
- ) {}
26
+ requestTimeoutMs: number = DEFAULT_WEB3SIGNER_REQUEST_TIMEOUT_MS,
27
+ ) {
28
+ this.requestTimeoutMs = requestTimeoutMs;
29
+ }
22
30
 
23
31
  /**
24
32
  * Get the address of a signer by index
@@ -108,75 +116,50 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
108
116
  * @param data - The data to sign
109
117
  * @returns The signature
110
118
  */
111
- private async makeJsonRpcSignRequest(address: EthAddress, data: Buffer32): Promise<Signature> {
112
- const url = this.baseUrl;
113
-
114
- // Use JSON-RPC eth_sign method which automatically applies Ethereum message prefixing
115
- const body = {
119
+ private makeJsonRpcSignRequest(address: EthAddress, data: Buffer32): Promise<Signature> {
120
+ // eth_sign automatically applies Ethereum message prefixing to the raw data.
121
+ return this.sendSignRequest({
116
122
  jsonrpc: '2.0',
117
123
  method: 'eth_sign',
118
- params: [
119
- address.toString(), // Ethereum address as identifier
120
- data.toString(), // Raw data to sign (eth_sign will apply Ethereum message prefix)
121
- ],
124
+ params: [address.toString(), data.toString()],
122
125
  id: 1,
123
- };
124
-
125
- const response = await fetch(url, {
126
- method: 'POST',
127
- headers: {
128
- 'Content-Type': 'application/json',
129
- },
130
- body: JSON.stringify(body),
131
126
  });
132
-
133
- if (!response.ok) {
134
- const errorText = await response.text();
135
- throw new Error(`Web3Signer request failed: ${response.status} ${response.statusText} - ${errorText}`);
136
- }
137
-
138
- const result = await response.json();
139
-
140
- // Handle JSON-RPC response format
141
- if (result.error) {
142
- throw new Error(`Web3Signer JSON-RPC error: ${result.error.code} - ${result.error.message}`);
143
- }
144
-
145
- if (!result.result) {
146
- throw new Error('Invalid response from Web3Signer: no result found');
147
- }
148
-
149
- let signatureHex = result.result;
150
-
151
- // Ensure the signature has the 0x prefix
152
- if (!signatureHex.startsWith('0x')) {
153
- signatureHex = '0x' + signatureHex;
154
- }
155
-
156
- // Parse the signature from the hex string
157
- return normalizeSignature(Signature.fromString(signatureHex as `0x${string}`));
158
127
  }
159
128
 
160
- private async makeJsonRpcSignTypedDataRequest(
161
- address: EthAddress,
162
- typedData: TypedDataDefinition,
163
- ): Promise<Signature> {
164
- const url = this.baseUrl;
165
-
166
- const body = {
129
+ private makeJsonRpcSignTypedDataRequest(address: EthAddress, typedData: TypedDataDefinition): Promise<Signature> {
130
+ return this.sendSignRequest({
167
131
  jsonrpc: '2.0',
168
132
  method: 'eth_signTypedData',
169
133
  params: [address.toString(), JSON.stringify(typedData)],
170
134
  id: 1,
171
- };
172
-
173
- const response = await fetch(url, {
174
- method: 'POST',
175
- headers: {
176
- 'Content-Type': 'application/json',
177
- },
178
- body: JSON.stringify(body),
179
135
  });
136
+ }
137
+
138
+ /**
139
+ * Send a JSON-RPC request to Web3Signer under a hard request timeout and parse the signature.
140
+ * A timed-out or aborted request is surfaced as a clear timeout error rather than hanging, so a
141
+ * slow or unreachable signer cannot stall an HA signing operation past its own timeout budget.
142
+ */
143
+ private async sendSignRequest(body: object): Promise<Signature> {
144
+ let response: Response;
145
+ try {
146
+ response = await fetch(this.baseUrl, {
147
+ method: 'POST',
148
+ headers: {
149
+ 'Content-Type': 'application/json',
150
+ },
151
+ body: JSON.stringify(body),
152
+ signal: AbortSignal.timeout(this.requestTimeoutMs),
153
+ });
154
+ } catch (err) {
155
+ if (
156
+ (err instanceof Error || err instanceof DOMException) &&
157
+ (err.name === 'TimeoutError' || err.name === 'AbortError')
158
+ ) {
159
+ throw new Error(`Web3Signer request timed out after ${this.requestTimeoutMs}ms`);
160
+ }
161
+ throw err;
162
+ }
180
163
 
181
164
  if (!response.ok) {
182
165
  const errorText = await response.text();
@@ -185,6 +168,7 @@ export class Web3SignerKeyStore implements ValidatorKeyStore {
185
168
 
186
169
  const result = await response.json();
187
170
 
171
+ // Handle JSON-RPC response format
188
172
  if (result.error) {
189
173
  throw new Error(`Web3Signer JSON-RPC error: ${result.error.code} - ${result.error.message}`);
190
174
  }