@aztec/validator-ha-signer 0.0.1-commit.f504929 → 0.0.1-commit.f5a9928

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 (50) hide show
  1. package/README.md +2 -4
  2. package/dest/db/index.d.ts +2 -1
  3. package/dest/db/index.d.ts.map +1 -1
  4. package/dest/db/index.js +1 -0
  5. package/dest/db/lmdb.d.ts +70 -0
  6. package/dest/db/lmdb.d.ts.map +1 -0
  7. package/dest/db/lmdb.js +223 -0
  8. package/dest/db/migrations/1_initial-schema.d.ts +4 -2
  9. package/dest/db/migrations/1_initial-schema.d.ts.map +1 -1
  10. package/dest/db/migrations/1_initial-schema.js +34 -4
  11. package/dest/db/migrations/2_add-checkpoint-number.d.ts +7 -0
  12. package/dest/db/migrations/2_add-checkpoint-number.d.ts.map +1 -0
  13. package/dest/db/migrations/2_add-checkpoint-number.js +17 -0
  14. package/dest/db/postgres.d.ts +4 -2
  15. package/dest/db/postgres.d.ts.map +1 -1
  16. package/dest/db/postgres.js +15 -13
  17. package/dest/db/schema.d.ts +6 -6
  18. package/dest/db/schema.d.ts.map +1 -1
  19. package/dest/db/schema.js +9 -4
  20. package/dest/db/types.d.ts +44 -7
  21. package/dest/db/types.d.ts.map +1 -1
  22. package/dest/db/types.js +26 -0
  23. package/dest/errors.d.ts +14 -1
  24. package/dest/errors.d.ts.map +1 -1
  25. package/dest/errors.js +15 -0
  26. package/dest/factory.d.ts +38 -5
  27. package/dest/factory.d.ts.map +1 -1
  28. package/dest/factory.js +95 -9
  29. package/dest/slashing_protection_service.d.ts +6 -4
  30. package/dest/slashing_protection_service.d.ts.map +1 -1
  31. package/dest/slashing_protection_service.js +14 -11
  32. package/dest/types.d.ts +8 -3
  33. package/dest/types.d.ts.map +1 -1
  34. package/dest/types.js +2 -1
  35. package/dest/validator_ha_signer.d.ts +5 -4
  36. package/dest/validator_ha_signer.d.ts.map +1 -1
  37. package/dest/validator_ha_signer.js +33 -12
  38. package/package.json +9 -7
  39. package/src/db/index.ts +1 -0
  40. package/src/db/lmdb.ts +308 -0
  41. package/src/db/migrations/1_initial-schema.ts +35 -4
  42. package/src/db/migrations/2_add-checkpoint-number.ts +19 -0
  43. package/src/db/postgres.ts +15 -11
  44. package/src/db/schema.ts +9 -4
  45. package/src/db/types.ts +63 -6
  46. package/src/errors.ts +21 -0
  47. package/src/factory.ts +130 -7
  48. package/src/slashing_protection_service.ts +18 -13
  49. package/src/types.ts +11 -0
  50. package/src/validator_ha_signer.ts +48 -13
package/src/types.ts CHANGED
@@ -2,11 +2,14 @@ import { SlotNumber } from '@aztec/foundation/branded-types';
2
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import { DateProvider } from '@aztec/foundation/timer';
4
4
  import {
5
+ type AttestationSigningContext,
6
+ type CheckpointProposalSigningContext,
5
7
  DutyType,
6
8
  type HAProtectedSigningContext,
7
9
  type SigningContext,
8
10
  type ValidatorHASignerConfig,
9
11
  getBlockNumberFromSigningContext as getBlockNumberFromSigningContextFromStdlib,
12
+ getCheckpointNumberFromSigningContext as getCheckpointNumberFromSigningContextFromStdlib,
10
13
  isHAProtectedContext,
11
14
  } from '@aztec/stdlib/ha-signing';
12
15
  import type { TelemetryClient } from '@aztec/telemetry-client';
@@ -25,8 +28,10 @@ import type {
25
28
  } from './db/types.js';
26
29
 
27
30
  export type {
31
+ AttestationSigningContext,
28
32
  BlockProposalDutyIdentifier,
29
33
  CheckAndRecordParams,
34
+ CheckpointProposalSigningContext,
30
35
  DeleteDutyParams,
31
36
  DutyIdentifier,
32
37
  DutyRow,
@@ -40,6 +45,7 @@ export type {
40
45
  export { DutyStatus, DutyType, getBlockIndexFromDutyIdentifier, normalizeBlockIndex } from './db/types.js';
41
46
  export { isHAProtectedContext };
42
47
  export { getBlockNumberFromSigningContextFromStdlib as getBlockNumberFromSigningContext };
48
+ export { getCheckpointNumberFromSigningContextFromStdlib as getCheckpointNumberFromSigningContext };
43
49
 
44
50
  /**
45
51
  * Result of tryInsertOrGetExisting operation
@@ -70,6 +76,11 @@ export interface CreateHASignerDeps {
70
76
  dateProvider?: DateProvider;
71
77
  }
72
78
 
79
+ /**
80
+ * deps for creating a local signing protection signer
81
+ */
82
+ export type CreateLocalSignerWithProtectionDeps = Omit<CreateHASignerDeps, 'pool'>;
83
+
73
84
  /**
74
85
  * Database interface for slashing protection operations
75
86
  * This abstraction allows for different database implementations (PostgreSQL, SQLite, etc.)
@@ -9,17 +9,19 @@ import type { Buffer32 } from '@aztec/foundation/buffer';
9
9
  import { EthAddress } from '@aztec/foundation/eth-address';
10
10
  import type { Signature } from '@aztec/foundation/eth-signature';
11
11
  import { type Logger, createLogger } from '@aztec/foundation/log';
12
- import type { DateProvider } from '@aztec/foundation/timer';
12
+ import { type DateProvider, executeTimeout } from '@aztec/foundation/timer';
13
13
  import {
14
+ type BaseSignerConfig,
14
15
  DutyType,
15
16
  type HAProtectedSigningContext,
16
- type ValidatorHASignerConfig,
17
17
  getBlockNumberFromSigningContext,
18
+ getCheckpointNumberFromSigningContext,
18
19
  } from '@aztec/stdlib/ha-signing';
19
20
 
20
21
  import type { DutyIdentifier } from './db/types.js';
22
+ import { SigningLockLostError } from './errors.js';
21
23
  import type { HASignerMetrics } from './metrics.js';
22
- import { SlashingProtectionService } from './slashing_protection_service.js';
24
+ import { DEFAULT_MAX_STUCK_DUTIES_AGE_MS, SlashingProtectionService } from './slashing_protection_service.js';
23
25
  import type { SlashingProtectionDatabase } from './types.js';
24
26
 
25
27
  export interface ValidatorHASignerDeps {
@@ -27,6 +29,9 @@ export interface ValidatorHASignerDeps {
27
29
  dateProvider: DateProvider;
28
30
  }
29
31
 
32
+ /** Default hard timeout (ms) for a single signer call when not configured. */
33
+ const DEFAULT_SIGNER_CALL_TIMEOUT_MS = 30_000;
34
+
30
35
  /**
31
36
  * Validator High Availability Signer
32
37
  *
@@ -53,10 +58,11 @@ export class ValidatorHASigner {
53
58
 
54
59
  private readonly dateProvider: DateProvider;
55
60
  private readonly metrics: HASignerMetrics;
61
+ private readonly signerCallTimeoutMs: number;
56
62
 
57
63
  constructor(
58
64
  db: SlashingProtectionDatabase,
59
- private readonly config: ValidatorHASignerConfig,
65
+ private readonly config: BaseSignerConfig,
60
66
  deps: ValidatorHASignerDeps,
61
67
  ) {
62
68
  this.log = createLogger('validator-ha-signer');
@@ -64,22 +70,31 @@ export class ValidatorHASigner {
64
70
  this.metrics = deps.metrics;
65
71
  this.dateProvider = deps.dateProvider;
66
72
 
67
- if (!config.haSigningEnabled) {
68
- // this shouldn't happen, the validator should use different signer for non-HA setups
69
- throw new Error('Validator HA Signer is not enabled in config');
70
- }
73
+ // Clamp the signer-call timeout below half the stuck-duty max age. This maintains the
74
+ // invariant that an in-flight signing always times out and releases its SIGNING row well before
75
+ // stuck-duty cleanup could consider it stuck, so cleanup can never delete a live duty (only
76
+ // signWithProtection writes SIGNING rows, and every path through it is bounded by this timeout).
77
+ // If timers misbehave anyway, the recordSuccess-returns-false throw is the backstop: the duty
78
+ // fails instead of broadcasting an unprotected signature.
79
+ const maxStuckDutiesAgeMs = config.maxStuckDutiesAgeMs ?? DEFAULT_MAX_STUCK_DUTIES_AGE_MS;
80
+ this.signerCallTimeoutMs = Math.min(
81
+ config.signerCallTimeoutMs ?? DEFAULT_SIGNER_CALL_TIMEOUT_MS,
82
+ maxStuckDutiesAgeMs / 2,
83
+ );
71
84
 
72
85
  if (!config.nodeId || config.nodeId === '') {
73
86
  throw new Error('NODE_ID is required for high-availability setups');
74
87
  }
75
- this.rollupAddress = config.l1Contracts.rollupAddress;
88
+ this.rollupAddress = config.rollupAddress;
76
89
  this.slashingProtection = new SlashingProtectionService(db, config, {
77
90
  metrics: deps.metrics,
78
91
  dateProvider: deps.dateProvider,
79
92
  });
93
+
80
94
  this.log.info('Validator HA Signer initialized with slashing protection', {
81
95
  nodeId: config.nodeId,
82
96
  rollupAddress: this.rollupAddress.toString(),
97
+ signerCallTimeoutMs: this.signerCallTimeoutMs,
83
98
  });
84
99
  }
85
100
 
@@ -130,17 +145,29 @@ export class ValidatorHASigner {
130
145
  // Acquire lock and get the token for ownership verification
131
146
  // DutyAlreadySignedError and SlashingProtectionError may be thrown here and are recorded in the service
132
147
  const blockNumber = getBlockNumberFromSigningContext(context);
148
+ const checkpointNumber = getCheckpointNumberFromSigningContext(context);
133
149
  const lockToken = await this.slashingProtection.checkAndRecord({
134
150
  ...dutyIdentifier,
135
151
  blockNumber,
152
+ checkpointNumber,
136
153
  messageHash: messageHash.toString(),
137
154
  nodeId: this.config.nodeId,
138
155
  });
139
156
 
140
- // Perform signing
157
+ // Perform signing under a hard timeout. If the signer hangs, executeTimeout aborts and rejects;
158
+ // the orphaned signFn promise resolving later is discarded (never broadcast). A timeout takes the
159
+ // same failure path as any signing error: release the lock so the duty can be retried safely.
141
160
  let signature: Signature;
142
161
  try {
143
- signature = await signFn(messageHash);
162
+ signature = await executeTimeout(
163
+ () => signFn(messageHash),
164
+ this.signerCallTimeoutMs,
165
+ () =>
166
+ new Error(
167
+ `Signing operation for ${dutyType} at slot ${context.slot} timed out after ` +
168
+ `${this.signerCallTimeoutMs}ms`,
169
+ ),
170
+ );
144
171
  } catch (error: any) {
145
172
  // Delete duty to allow retry (only succeeds if we own the lock)
146
173
  await this.slashingProtection.deleteDuty({ ...dutyIdentifier, lockToken });
@@ -148,13 +175,21 @@ export class ValidatorHASigner {
148
175
  throw error;
149
176
  }
150
177
 
151
- // Record success (only succeeds if we own the lock)
152
- await this.slashingProtection.recordSuccess({
178
+ // Record success (only succeeds if we still own the lock).
179
+ // A false result means our SIGNING row is gone or no longer ours (e.g. deleted by stuck-duty
180
+ // cleanup while signing was slow). We must not broadcast this signature: without a protection
181
+ // record, a later attempt for the same duty with different data would sign freely (slashable).
182
+ // Do not delete the duty here - we no longer own it, and another node may legitimately hold it.
183
+ const recorded = await this.slashingProtection.recordSuccess({
153
184
  ...dutyIdentifier,
154
185
  signature,
155
186
  nodeId: this.config.nodeId,
156
187
  lockToken,
157
188
  });
189
+ if (!recorded) {
190
+ this.metrics.recordSigningError(dutyType);
191
+ throw new SigningLockLostError(context.slot, dutyType, this.config.nodeId);
192
+ }
158
193
 
159
194
  const duration = this.dateProvider.now() - startTime;
160
195
  this.metrics.recordSigningSuccess(dutyType, duration);