@aztec/validator-ha-signer 0.0.1-commit.023c3e5

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 (58) hide show
  1. package/README.md +187 -0
  2. package/dest/config.d.ts +79 -0
  3. package/dest/config.d.ts.map +1 -0
  4. package/dest/config.js +73 -0
  5. package/dest/db/index.d.ts +4 -0
  6. package/dest/db/index.d.ts.map +1 -0
  7. package/dest/db/index.js +3 -0
  8. package/dest/db/migrations/1_initial-schema.d.ts +9 -0
  9. package/dest/db/migrations/1_initial-schema.d.ts.map +1 -0
  10. package/dest/db/migrations/1_initial-schema.js +20 -0
  11. package/dest/db/postgres.d.ts +70 -0
  12. package/dest/db/postgres.d.ts.map +1 -0
  13. package/dest/db/postgres.js +181 -0
  14. package/dest/db/schema.d.ts +89 -0
  15. package/dest/db/schema.d.ts.map +1 -0
  16. package/dest/db/schema.js +213 -0
  17. package/dest/db/test_helper.d.ts +10 -0
  18. package/dest/db/test_helper.d.ts.map +1 -0
  19. package/dest/db/test_helper.js +14 -0
  20. package/dest/db/types.d.ts +161 -0
  21. package/dest/db/types.d.ts.map +1 -0
  22. package/dest/db/types.js +49 -0
  23. package/dest/errors.d.ts +34 -0
  24. package/dest/errors.d.ts.map +1 -0
  25. package/dest/errors.js +34 -0
  26. package/dest/factory.d.ts +42 -0
  27. package/dest/factory.d.ts.map +1 -0
  28. package/dest/factory.js +70 -0
  29. package/dest/migrations.d.ts +15 -0
  30. package/dest/migrations.d.ts.map +1 -0
  31. package/dest/migrations.js +53 -0
  32. package/dest/slashing_protection_service.d.ts +80 -0
  33. package/dest/slashing_protection_service.d.ts.map +1 -0
  34. package/dest/slashing_protection_service.js +196 -0
  35. package/dest/test/pglite_pool.d.ts +92 -0
  36. package/dest/test/pglite_pool.d.ts.map +1 -0
  37. package/dest/test/pglite_pool.js +210 -0
  38. package/dest/types.d.ts +139 -0
  39. package/dest/types.d.ts.map +1 -0
  40. package/dest/types.js +21 -0
  41. package/dest/validator_ha_signer.d.ts +70 -0
  42. package/dest/validator_ha_signer.d.ts.map +1 -0
  43. package/dest/validator_ha_signer.js +127 -0
  44. package/package.json +105 -0
  45. package/src/config.ts +125 -0
  46. package/src/db/index.ts +3 -0
  47. package/src/db/migrations/1_initial-schema.ts +26 -0
  48. package/src/db/postgres.ts +251 -0
  49. package/src/db/schema.ts +248 -0
  50. package/src/db/test_helper.ts +17 -0
  51. package/src/db/types.ts +201 -0
  52. package/src/errors.ts +47 -0
  53. package/src/factory.ts +82 -0
  54. package/src/migrations.ts +75 -0
  55. package/src/slashing_protection_service.ts +250 -0
  56. package/src/test/pglite_pool.ts +256 -0
  57. package/src/types.ts +207 -0
  58. package/src/validator_ha_signer.ts +157 -0
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Validator High Availability Signer
3
+ *
4
+ * Wraps signing operations with distributed locking and slashing protection.
5
+ * This ensures that even with multiple validator nodes running, only one
6
+ * node will sign for a given duty (slot + duty type).
7
+ */
8
+ import type { Buffer32 } from '@aztec/foundation/buffer';
9
+ import type { EthAddress } from '@aztec/foundation/eth-address';
10
+ import type { Signature } from '@aztec/foundation/eth-signature';
11
+ import { type Logger, createLogger } from '@aztec/foundation/log';
12
+
13
+ import type { ValidatorHASignerConfig } from './config.js';
14
+ import { type DutyIdentifier, DutyType } from './db/types.js';
15
+ import { SlashingProtectionService } from './slashing_protection_service.js';
16
+ import {
17
+ type HAProtectedSigningContext,
18
+ type SlashingProtectionDatabase,
19
+ getBlockNumberFromSigningContext,
20
+ } from './types.js';
21
+
22
+ /**
23
+ * Validator High Availability Signer
24
+ *
25
+ * Provides signing capabilities with distributed locking for validators
26
+ * in a high-availability setup.
27
+ *
28
+ * Usage:
29
+ * ```
30
+ * const signer = new ValidatorHASigner(db, config);
31
+ *
32
+ * // Sign with slashing protection
33
+ * const signature = await signer.signWithProtection(
34
+ * validatorAddress,
35
+ * messageHash,
36
+ * { slot: 100n, blockNumber: 50n, dutyType: 'BLOCK_PROPOSAL' },
37
+ * async (root) => localSigner.signMessage(root),
38
+ * );
39
+ * ```
40
+ */
41
+ export class ValidatorHASigner {
42
+ private readonly log: Logger;
43
+ private readonly slashingProtection: SlashingProtectionService;
44
+
45
+ constructor(
46
+ db: SlashingProtectionDatabase,
47
+ private readonly config: ValidatorHASignerConfig,
48
+ ) {
49
+ this.log = createLogger('validator-ha-signer');
50
+
51
+ if (!config.haSigningEnabled) {
52
+ // this shouldn't happen, the validator should use different signer for non-HA setups
53
+ throw new Error('Validator HA Signer is not enabled in config');
54
+ }
55
+
56
+ if (!config.nodeId || config.nodeId === '') {
57
+ throw new Error('NODE_ID is required for high-availability setups');
58
+ }
59
+ this.slashingProtection = new SlashingProtectionService(db, config);
60
+ this.log.info('Validator HA Signer initialized with slashing protection', {
61
+ nodeId: config.nodeId,
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Sign a message with slashing protection.
67
+ *
68
+ * This method:
69
+ * 1. Acquires a distributed lock for (validator, slot, dutyType)
70
+ * 2. Calls the provided signing function
71
+ * 3. Records the result (success or failure)
72
+ *
73
+ * @param validatorAddress - The validator's Ethereum address
74
+ * @param messageHash - The hash to be signed
75
+ * @param context - The signing context (HA-protected duty types only)
76
+ * @param signFn - Function that performs the actual signing
77
+ * @returns The signature
78
+ *
79
+ * @throws DutyAlreadySignedError if the duty was already signed (expected in HA)
80
+ * @throws SlashingProtectionError if attempting to sign different data for same slot (expected in HA)
81
+ */
82
+ async signWithProtection(
83
+ validatorAddress: EthAddress,
84
+ messageHash: Buffer32,
85
+ context: HAProtectedSigningContext,
86
+ signFn: (messageHash: Buffer32) => Promise<Signature>,
87
+ ): Promise<Signature> {
88
+ let dutyIdentifier: DutyIdentifier;
89
+ if (context.dutyType === DutyType.BLOCK_PROPOSAL) {
90
+ dutyIdentifier = {
91
+ validatorAddress,
92
+ slot: context.slot,
93
+ blockIndexWithinCheckpoint: context.blockIndexWithinCheckpoint,
94
+ dutyType: context.dutyType,
95
+ };
96
+ } else {
97
+ dutyIdentifier = {
98
+ validatorAddress,
99
+ slot: context.slot,
100
+ dutyType: context.dutyType,
101
+ };
102
+ }
103
+
104
+ // Acquire lock and get the token for ownership verification
105
+ const blockNumber = getBlockNumberFromSigningContext(context);
106
+ const lockToken = await this.slashingProtection.checkAndRecord({
107
+ ...dutyIdentifier,
108
+ blockNumber,
109
+ messageHash: messageHash.toString(),
110
+ nodeId: this.config.nodeId,
111
+ });
112
+
113
+ // Perform signing
114
+ let signature: Signature;
115
+ try {
116
+ signature = await signFn(messageHash);
117
+ } catch (error: any) {
118
+ // Delete duty to allow retry (only succeeds if we own the lock)
119
+ await this.slashingProtection.deleteDuty({ ...dutyIdentifier, lockToken });
120
+ throw error;
121
+ }
122
+
123
+ // Record success (only succeeds if we own the lock)
124
+ await this.slashingProtection.recordSuccess({
125
+ ...dutyIdentifier,
126
+ signature,
127
+ nodeId: this.config.nodeId,
128
+ lockToken,
129
+ });
130
+
131
+ return signature;
132
+ }
133
+
134
+ /**
135
+ * Get the node ID for this signer
136
+ */
137
+ get nodeId(): string {
138
+ return this.config.nodeId;
139
+ }
140
+
141
+ /**
142
+ * Start the HA signer background tasks (cleanup of stuck duties).
143
+ * Should be called after construction and before signing operations.
144
+ */
145
+ start() {
146
+ this.slashingProtection.start();
147
+ }
148
+
149
+ /**
150
+ * Stop the HA signer background tasks and close database connection.
151
+ * Should be called during graceful shutdown.
152
+ */
153
+ async stop() {
154
+ await this.slashingProtection.stop();
155
+ await this.slashingProtection.close();
156
+ }
157
+ }