@aztec/validator-ha-signer 0.0.1-commit.001888fc

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 (62) hide show
  1. package/README.md +195 -0
  2. package/dest/db/index.d.ts +5 -0
  3. package/dest/db/index.d.ts.map +1 -0
  4. package/dest/db/index.js +4 -0
  5. package/dest/db/lmdb.d.ts +66 -0
  6. package/dest/db/lmdb.d.ts.map +1 -0
  7. package/dest/db/lmdb.js +188 -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 +86 -0
  12. package/dest/db/postgres.d.ts.map +1 -0
  13. package/dest/db/postgres.js +208 -0
  14. package/dest/db/schema.d.ts +96 -0
  15. package/dest/db/schema.d.ts.map +1 -0
  16. package/dest/db/schema.js +230 -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 +185 -0
  21. package/dest/db/types.d.ts.map +1 -0
  22. package/dest/db/types.js +64 -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 +60 -0
  27. package/dest/factory.d.ts.map +1 -0
  28. package/dest/factory.js +115 -0
  29. package/dest/metrics.d.ts +51 -0
  30. package/dest/metrics.d.ts.map +1 -0
  31. package/dest/metrics.js +103 -0
  32. package/dest/migrations.d.ts +15 -0
  33. package/dest/migrations.d.ts.map +1 -0
  34. package/dest/migrations.js +53 -0
  35. package/dest/slashing_protection_service.d.ts +93 -0
  36. package/dest/slashing_protection_service.d.ts.map +1 -0
  37. package/dest/slashing_protection_service.js +236 -0
  38. package/dest/test/pglite_pool.d.ts +92 -0
  39. package/dest/test/pglite_pool.d.ts.map +1 -0
  40. package/dest/test/pglite_pool.js +210 -0
  41. package/dest/types.d.ts +99 -0
  42. package/dest/types.d.ts.map +1 -0
  43. package/dest/types.js +4 -0
  44. package/dest/validator_ha_signer.d.ts +79 -0
  45. package/dest/validator_ha_signer.d.ts.map +1 -0
  46. package/dest/validator_ha_signer.js +140 -0
  47. package/package.json +110 -0
  48. package/src/db/index.ts +4 -0
  49. package/src/db/lmdb.ts +264 -0
  50. package/src/db/migrations/1_initial-schema.ts +26 -0
  51. package/src/db/postgres.ts +284 -0
  52. package/src/db/schema.ts +267 -0
  53. package/src/db/test_helper.ts +17 -0
  54. package/src/db/types.ts +251 -0
  55. package/src/errors.ts +47 -0
  56. package/src/factory.ts +139 -0
  57. package/src/metrics.ts +138 -0
  58. package/src/migrations.ts +75 -0
  59. package/src/slashing_protection_service.ts +308 -0
  60. package/src/test/pglite_pool.ts +256 -0
  61. package/src/types.ts +154 -0
  62. package/src/validator_ha_signer.ts +183 -0
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Vendored pg-compatible Pool/Client wrapper for PGlite.
3
+ *
4
+ * Copied from @middle-management/pglite-pg-adapter v0.0.3
5
+ * https://www.npmjs.com/package/@middle-management/pglite-pg-adapter
6
+ *
7
+ * Modifications:
8
+ * - Converted to ESM and TypeScript
9
+ * - Uses PGliteInterface instead of PGlite class to avoid TypeScript
10
+ * type mismatches from ESM/CJS dual package resolution with private fields
11
+ * - Simplified rowCount calculation to handle CTEs properly
12
+ */ import { EventEmitter } from 'events';
13
+ import { Readable, Writable } from 'stream';
14
+ export class Client extends EventEmitter {
15
+ pglite;
16
+ _connected = false;
17
+ host;
18
+ port;
19
+ ssl;
20
+ connection;
21
+ // Stub implementations for pg compatibility
22
+ copyFrom = ()=>new Writable();
23
+ copyTo = ()=>new Readable();
24
+ pauseDrain = ()=>{};
25
+ resumeDrain = ()=>{};
26
+ escapeLiteral = (str)=>`'${str.replace(/'/g, "''")}'`;
27
+ escapeIdentifier = (str)=>`"${str.replace(/"/g, '""')}"`;
28
+ setTypeParser = ()=>{};
29
+ getTypeParser = ()=>(value)=>value;
30
+ constructor(config){
31
+ super();
32
+ this.pglite = config.pglite;
33
+ this.host = config.host || 'localhost';
34
+ this.port = config.port || 5432;
35
+ this.ssl = typeof config.ssl === 'boolean' ? config.ssl : !!config.ssl;
36
+ this.connection = {};
37
+ }
38
+ connect() {
39
+ if (this._connected) {
40
+ return Promise.resolve();
41
+ }
42
+ this._connected = true;
43
+ this.emit('connect');
44
+ return Promise.resolve();
45
+ }
46
+ end() {
47
+ if (!this._connected) {
48
+ return Promise.resolve();
49
+ }
50
+ this._connected = false;
51
+ this.emit('end');
52
+ return Promise.resolve();
53
+ }
54
+ async query(text, values) {
55
+ if (!this._connected) {
56
+ throw new Error('Client is not connected');
57
+ }
58
+ const result = await this.pglite.query(text, values);
59
+ return this.convertPGliteResult(result);
60
+ }
61
+ convertPGliteResult(result) {
62
+ return {
63
+ command: '',
64
+ rowCount: 'affectedRows' in result ? result.affectedRows ?? 0 : result.rows.length,
65
+ oid: 0,
66
+ fields: result.fields.map((field)=>({
67
+ name: field.name,
68
+ tableID: 0,
69
+ columnID: 0,
70
+ dataTypeID: field.dataTypeID,
71
+ dataTypeSize: -1,
72
+ dataTypeModifier: -1,
73
+ format: 'text'
74
+ })),
75
+ rows: result.rows
76
+ };
77
+ }
78
+ get connected() {
79
+ return this._connected;
80
+ }
81
+ }
82
+ export class Pool extends EventEmitter {
83
+ clients = [];
84
+ availableClients = [];
85
+ waitingQueue = [];
86
+ _ended = false;
87
+ pglite;
88
+ _config;
89
+ expiredCount = 0;
90
+ options;
91
+ constructor(config){
92
+ super();
93
+ this._config = {
94
+ max: 10,
95
+ min: 0,
96
+ ...config
97
+ };
98
+ this.pglite = config.pglite;
99
+ this.options = config;
100
+ }
101
+ get totalCount() {
102
+ return this.clients.length;
103
+ }
104
+ get idleCount() {
105
+ return this.availableClients.length;
106
+ }
107
+ get waitingCount() {
108
+ return this.waitingQueue.length;
109
+ }
110
+ get ending() {
111
+ return this._ended;
112
+ }
113
+ get ended() {
114
+ return this._ended;
115
+ }
116
+ connect() {
117
+ if (this._ended) {
118
+ return Promise.reject(new Error('Pool is ended'));
119
+ }
120
+ if (this.availableClients.length > 0) {
121
+ const client = this.availableClients.pop();
122
+ client._markInUse();
123
+ return Promise.resolve(client);
124
+ }
125
+ if (this.clients.length < (this._config.max || 10)) {
126
+ const client = new PoolClient(this.pglite, this);
127
+ this.clients.push(client);
128
+ return Promise.resolve(client);
129
+ }
130
+ return new Promise((resolve)=>{
131
+ this.waitingQueue.push(resolve);
132
+ });
133
+ }
134
+ async query(text, values) {
135
+ const client = await this.connect();
136
+ try {
137
+ return await client.query(text, values);
138
+ } finally{
139
+ client.release();
140
+ }
141
+ }
142
+ releaseClient(client) {
143
+ const index = this.clients.indexOf(client);
144
+ if (index !== -1) {
145
+ client._markAvailable();
146
+ if (this.waitingQueue.length > 0) {
147
+ const resolve = this.waitingQueue.shift();
148
+ client._markInUse();
149
+ resolve(client);
150
+ } else {
151
+ this.availableClients.push(client);
152
+ }
153
+ }
154
+ }
155
+ end() {
156
+ this._ended = true;
157
+ this.clients.forEach((client)=>client._markReleased());
158
+ this.clients = [];
159
+ this.availableClients = [];
160
+ this.emit('end');
161
+ return Promise.resolve();
162
+ }
163
+ }
164
+ export class PoolClient extends Client {
165
+ pool;
166
+ _released = false;
167
+ _inUse = true;
168
+ _userReleased = false;
169
+ constructor(pglite, pool){
170
+ super({
171
+ pglite
172
+ });
173
+ this.pool = pool;
174
+ this._connected = true;
175
+ }
176
+ async query(text, values) {
177
+ if (this._userReleased && !this._inUse) {
178
+ throw new Error('Client has been released back to the pool');
179
+ }
180
+ const result = await this.pglite.query(text, values);
181
+ return this.convertPGliteResult(result);
182
+ }
183
+ release() {
184
+ if (this._released || this._userReleased) {
185
+ return;
186
+ }
187
+ this._userReleased = true;
188
+ this.pool.releaseClient(this);
189
+ }
190
+ end() {
191
+ this.release();
192
+ return Promise.resolve();
193
+ }
194
+ _markInUse() {
195
+ this._inUse = true;
196
+ this._userReleased = false;
197
+ }
198
+ _markAvailable() {
199
+ this._inUse = false;
200
+ this._userReleased = false;
201
+ }
202
+ _markReleased() {
203
+ this._released = true;
204
+ this._inUse = false;
205
+ this._userReleased = true;
206
+ }
207
+ get connected() {
208
+ return this._connected && !this._released;
209
+ }
210
+ }
@@ -0,0 +1,99 @@
1
+ import { SlotNumber } from '@aztec/foundation/branded-types';
2
+ import type { EthAddress } from '@aztec/foundation/eth-address';
3
+ import { DateProvider } from '@aztec/foundation/timer';
4
+ import { DutyType, type HAProtectedSigningContext, type SigningContext, type ValidatorHASignerConfig, getBlockNumberFromSigningContext as getBlockNumberFromSigningContextFromStdlib, isHAProtectedContext } from '@aztec/stdlib/ha-signing';
5
+ import type { TelemetryClient } from '@aztec/telemetry-client';
6
+ import type { Pool } from 'pg';
7
+ import type { BlockProposalDutyIdentifier, CheckAndRecordParams, DeleteDutyParams, DutyIdentifier, DutyRow, OtherDutyIdentifier, RecordSuccessParams, ValidatorDutyRecord } from './db/types.js';
8
+ export type { BlockProposalDutyIdentifier, CheckAndRecordParams, DeleteDutyParams, DutyIdentifier, DutyRow, HAProtectedSigningContext, OtherDutyIdentifier, RecordSuccessParams, SigningContext, ValidatorDutyRecord, ValidatorHASignerConfig, };
9
+ export { DutyStatus, DutyType, getBlockIndexFromDutyIdentifier, normalizeBlockIndex } from './db/types.js';
10
+ export { isHAProtectedContext };
11
+ export { getBlockNumberFromSigningContextFromStdlib as getBlockNumberFromSigningContext };
12
+ /**
13
+ * Result of tryInsertOrGetExisting operation
14
+ */
15
+ export interface TryInsertOrGetResult {
16
+ /** True if we inserted a new record, false if we got an existing record */
17
+ isNew: boolean;
18
+ /** The record (either newly inserted or existing) */
19
+ record: ValidatorDutyRecord;
20
+ }
21
+ /**
22
+ * deps for creating an HA signer
23
+ */
24
+ export interface CreateHASignerDeps {
25
+ /**
26
+ * Optional PostgreSQL connection pool
27
+ * If provided, databaseUrl and poolConfig are ignored
28
+ */
29
+ pool?: Pool;
30
+ /**
31
+ * Optional telemetry client for metrics
32
+ */
33
+ telemetryClient?: TelemetryClient;
34
+ /**
35
+ * Optional date provider for timestamps
36
+ */
37
+ dateProvider?: DateProvider;
38
+ }
39
+ /**
40
+ * deps for creating a local signing protection signer
41
+ */
42
+ export type CreateLocalSignerWithProtectionDeps = Omit<CreateHASignerDeps, 'pool'>;
43
+ /**
44
+ * Database interface for slashing protection operations
45
+ * This abstraction allows for different database implementations (PostgreSQL, SQLite, etc.)
46
+ *
47
+ * The interface is designed around 3 core operations:
48
+ * 1. tryInsertOrGetExisting - Atomically insert or get existing record (eliminates race conditions)
49
+ * 2. updateDutySigned - Update to signed status on success
50
+ * 3. deleteDuty - Delete a duty record on failure
51
+ */
52
+ export interface SlashingProtectionDatabase {
53
+ /**
54
+ * Atomically try to insert a new duty record, or get the existing one if present.
55
+ *
56
+ * @returns { isNew: true, record } if we successfully inserted and acquired the lock
57
+ * @returns { isNew: false, record } if a record already exists (caller should handle based on status)
58
+ */
59
+ tryInsertOrGetExisting(params: CheckAndRecordParams): Promise<TryInsertOrGetResult>;
60
+ /**
61
+ * Update a duty to 'signed' status with the signature.
62
+ * Only succeeds if the lockToken matches (caller must be the one who created the duty).
63
+ *
64
+ * @returns true if the update succeeded, false if token didn't match or duty not found
65
+ */
66
+ updateDutySigned(rollupAddress: EthAddress, validatorAddress: EthAddress, slot: SlotNumber, dutyType: DutyType, signature: string, lockToken: string, blockIndexWithinCheckpoint: number): Promise<boolean>;
67
+ /**
68
+ * Delete a duty record.
69
+ * Only succeeds if the lockToken matches (caller must be the one who created the duty).
70
+ * Used when signing fails to allow another node/attempt to retry.
71
+ *
72
+ * @returns true if the delete succeeded, false if token didn't match or duty not found
73
+ */
74
+ deleteDuty(rollupAddress: EthAddress, validatorAddress: EthAddress, slot: SlotNumber, dutyType: DutyType, lockToken: string, blockIndexWithinCheckpoint: number): Promise<boolean>;
75
+ /**
76
+ * Cleanup own stuck duties
77
+ * @returns the number of duties cleaned up
78
+ */
79
+ cleanupOwnStuckDuties(nodeId: string, maxAgeMs: number): Promise<number>;
80
+ /**
81
+ * Cleanup duties with outdated rollup address.
82
+ * Removes all duties where the rollup address doesn't match the current one.
83
+ * Used after a rollup upgrade to clean up duties for the old rollup.
84
+ * @returns the number of duties cleaned up
85
+ */
86
+ cleanupOutdatedRollupDuties(currentRollupAddress: EthAddress): Promise<number>;
87
+ /**
88
+ * Cleanup old signed duties.
89
+ * Removes only signed duties older than the specified age.
90
+ * @returns the number of duties cleaned up
91
+ */
92
+ cleanupOldDuties(maxAgeMs: number): Promise<number>;
93
+ /**
94
+ * Close the database connection.
95
+ * Should be called during graceful shutdown.
96
+ */
97
+ close(): Promise<void>;
98
+ }
99
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy90eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDN0QsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDaEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sRUFDTCxRQUFRLEVBQ1IsS0FBSyx5QkFBeUIsRUFDOUIsS0FBSyxjQUFjLEVBQ25CLEtBQUssdUJBQXVCLEVBQzVCLGdDQUFnQyxJQUFJLDBDQUEwQyxFQUM5RSxvQkFBb0IsRUFDckIsTUFBTSwwQkFBMEIsQ0FBQztBQUNsQyxPQUFPLEtBQUssRUFBRSxlQUFlLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUUvRCxPQUFPLEtBQUssRUFBRSxJQUFJLEVBQUUsTUFBTSxJQUFJLENBQUM7QUFFL0IsT0FBTyxLQUFLLEVBQ1YsMkJBQTJCLEVBQzNCLG9CQUFvQixFQUNwQixnQkFBZ0IsRUFDaEIsY0FBYyxFQUNkLE9BQU8sRUFDUCxtQkFBbUIsRUFDbkIsbUJBQW1CLEVBQ25CLG1CQUFtQixFQUNwQixNQUFNLGVBQWUsQ0FBQztBQUV2QixZQUFZLEVBQ1YsMkJBQTJCLEVBQzNCLG9CQUFvQixFQUNwQixnQkFBZ0IsRUFDaEIsY0FBYyxFQUNkLE9BQU8sRUFDUCx5QkFBeUIsRUFDekIsbUJBQW1CLEVBQ25CLG1CQUFtQixFQUNuQixjQUFjLEVBQ2QsbUJBQW1CLEVBQ25CLHVCQUF1QixHQUN4QixDQUFDO0FBQ0YsT0FBTyxFQUFFLFVBQVUsRUFBRSxRQUFRLEVBQUUsK0JBQStCLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFDM0csT0FBTyxFQUFFLG9CQUFvQixFQUFFLENBQUM7QUFDaEMsT0FBTyxFQUFFLDBDQUEwQyxJQUFJLGdDQUFnQyxFQUFFLENBQUM7QUFFMUY7O0dBRUc7QUFDSCxNQUFNLFdBQVcsb0JBQW9CO0lBQ25DLDJFQUEyRTtJQUMzRSxLQUFLLEVBQUUsT0FBTyxDQUFDO0lBQ2YscURBQXFEO0lBQ3JELE1BQU0sRUFBRSxtQkFBbUIsQ0FBQztDQUM3QjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxXQUFXLGtCQUFrQjtJQUNqQzs7O09BR0c7SUFDSCxJQUFJLENBQUMsRUFBRSxJQUFJLENBQUM7SUFDWjs7T0FFRztJQUNILGVBQWUsQ0FBQyxFQUFFLGVBQWUsQ0FBQztJQUNsQzs7T0FFRztJQUNILFlBQVksQ0FBQyxFQUFFLFlBQVksQ0FBQztDQUM3QjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxNQUFNLG1DQUFtQyxHQUFHLElBQUksQ0FBQyxrQkFBa0IsRUFBRSxNQUFNLENBQUMsQ0FBQztBQUVuRjs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sV0FBVywwQkFBMEI7SUFDekM7Ozs7O09BS0c7SUFDSCxzQkFBc0IsQ0FBQyxNQUFNLEVBQUUsb0JBQW9CLEdBQUcsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQUM7SUFFcEY7Ozs7O09BS0c7SUFDSCxnQkFBZ0IsQ0FDZCxhQUFhLEVBQUUsVUFBVSxFQUN6QixnQkFBZ0IsRUFBRSxVQUFVLEVBQzVCLElBQUksRUFBRSxVQUFVLEVBQ2hCLFFBQVEsRUFBRSxRQUFRLEVBQ2xCLFNBQVMsRUFBRSxNQUFNLEVBQ2pCLFNBQVMsRUFBRSxNQUFNLEVBQ2pCLDBCQUEwQixFQUFFLE1BQU0sR0FDakMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBRXBCOzs7Ozs7T0FNRztJQUNILFVBQVUsQ0FDUixhQUFhLEVBQUUsVUFBVSxFQUN6QixnQkFBZ0IsRUFBRSxVQUFVLEVBQzVCLElBQUksRUFBRSxVQUFVLEVBQ2hCLFFBQVEsRUFBRSxRQUFRLEVBQ2xCLFNBQVMsRUFBRSxNQUFNLEVBQ2pCLDBCQUEwQixFQUFFLE1BQU0sR0FDakMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBRXBCOzs7T0FHRztJQUNILHFCQUFxQixDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLE1BQU0sR0FBRyxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUM7SUFFekU7Ozs7O09BS0c7SUFDSCwyQkFBMkIsQ0FBQyxvQkFBb0IsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBRS9FOzs7O09BSUc7SUFDSCxnQkFBZ0IsQ0FBQyxRQUFRLEVBQUUsTUFBTSxHQUFHLE9BQU8sQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUVwRDs7O09BR0c7SUFDSCxLQUFLLElBQUksT0FBTyxDQUFDLElBQUksQ0FBQyxDQUFDO0NBQ3hCIn0=
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EACL,QAAQ,EACR,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAC5B,gCAAgC,IAAI,0CAA0C,EAC9E,oBAAoB,EACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAE/B,OAAO,KAAK,EACV,2BAA2B,EAC3B,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,OAAO,EACP,mBAAmB,EACnB,mBAAmB,EACnB,mBAAmB,EACpB,MAAM,eAAe,CAAC;AAEvB,YAAY,EACV,2BAA2B,EAC3B,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,OAAO,EACP,yBAAyB,EACzB,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,uBAAuB,GACxB,CAAC;AACF,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,+BAA+B,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAC3G,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAChC,OAAO,EAAE,0CAA0C,IAAI,gCAAgC,EAAE,CAAC;AAE1F;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,2EAA2E;IAC3E,KAAK,EAAE,OAAO,CAAC;IACf,qDAAqD;IACrD,MAAM,EAAE,mBAAmB,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;;OAEG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;OAEG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B;AAED;;GAEG;AACH,MAAM,MAAM,mCAAmC,GAAG,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;AAEnF;;;;;;;;GAQG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,sBAAsB,CAAC,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEpF;;;;;OAKG;IACH,gBAAgB,CACd,aAAa,EAAE,UAAU,EACzB,gBAAgB,EAAE,UAAU,EAC5B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,0BAA0B,EAAE,MAAM,GACjC,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB;;;;;;OAMG;IACH,UAAU,CACR,aAAa,EAAE,UAAU,EACzB,gBAAgB,EAAE,UAAU,EAC5B,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,EACjB,0BAA0B,EAAE,MAAM,GACjC,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzE;;;;;OAKG;IACH,2BAA2B,CAAC,oBAAoB,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE/E;;;;OAIG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEpD;;;OAGG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB"}
package/dest/types.js ADDED
@@ -0,0 +1,4 @@
1
+ import { getBlockNumberFromSigningContext as getBlockNumberFromSigningContextFromStdlib, isHAProtectedContext } from '@aztec/stdlib/ha-signing';
2
+ export { DutyStatus, DutyType, getBlockIndexFromDutyIdentifier, normalizeBlockIndex } from './db/types.js';
3
+ export { isHAProtectedContext };
4
+ export { getBlockNumberFromSigningContextFromStdlib as getBlockNumberFromSigningContext };
@@ -0,0 +1,79 @@
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 { EthAddress } from '@aztec/foundation/eth-address';
10
+ import type { Signature } from '@aztec/foundation/eth-signature';
11
+ import type { DateProvider } from '@aztec/foundation/timer';
12
+ import { type BaseSignerConfig, type HAProtectedSigningContext } from '@aztec/stdlib/ha-signing';
13
+ import type { HASignerMetrics } from './metrics.js';
14
+ import type { SlashingProtectionDatabase } from './types.js';
15
+ export interface ValidatorHASignerDeps {
16
+ metrics: HASignerMetrics;
17
+ dateProvider: DateProvider;
18
+ }
19
+ /**
20
+ * Validator High Availability Signer
21
+ *
22
+ * Provides signing capabilities with distributed locking for validators
23
+ * in a high-availability setup.
24
+ *
25
+ * Usage:
26
+ * ```
27
+ * const signer = new ValidatorHASigner(db, config);
28
+ *
29
+ * // Sign with slashing protection
30
+ * const signature = await signer.signWithProtection(
31
+ * validatorAddress,
32
+ * messageHash,
33
+ * { slot: 100n, blockNumber: 50n, dutyType: 'BLOCK_PROPOSAL' },
34
+ * async (root) => localSigner.signMessage(root),
35
+ * );
36
+ * ```
37
+ */
38
+ export declare class ValidatorHASigner {
39
+ private readonly config;
40
+ private readonly log;
41
+ private readonly slashingProtection;
42
+ private readonly rollupAddress;
43
+ private readonly dateProvider;
44
+ private readonly metrics;
45
+ constructor(db: SlashingProtectionDatabase, config: BaseSignerConfig, deps: ValidatorHASignerDeps);
46
+ /**
47
+ * Sign a message with slashing protection.
48
+ *
49
+ * This method:
50
+ * 1. Acquires a distributed lock for (validator, slot, dutyType)
51
+ * 2. Calls the provided signing function
52
+ * 3. Records the result (success or failure)
53
+ *
54
+ * @param validatorAddress - The validator's Ethereum address
55
+ * @param messageHash - The hash to be signed
56
+ * @param context - The signing context (HA-protected duty types only)
57
+ * @param signFn - Function that performs the actual signing
58
+ * @returns The signature
59
+ *
60
+ * @throws DutyAlreadySignedError if the duty was already signed (expected in HA)
61
+ * @throws SlashingProtectionError if attempting to sign different data for same slot (expected in HA)
62
+ */
63
+ signWithProtection(validatorAddress: EthAddress, messageHash: Buffer32, context: HAProtectedSigningContext, signFn: (messageHash: Buffer32) => Promise<Signature>): Promise<Signature>;
64
+ /**
65
+ * Get the node ID for this signer
66
+ */
67
+ get nodeId(): string;
68
+ /**
69
+ * Start the HA signer background tasks (cleanup of stuck duties).
70
+ * Should be called after construction and before signing operations.
71
+ */
72
+ start(): Promise<void>;
73
+ /**
74
+ * Stop the HA signer background tasks and close database connection.
75
+ * Should be called during graceful shutdown.
76
+ */
77
+ stop(): Promise<void>;
78
+ }
79
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9yX2hhX3NpZ25lci5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL3ZhbGlkYXRvcl9oYV9zaWduZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7OztHQU1HO0FBQ0gsT0FBTyxLQUFLLEVBQUUsUUFBUSxFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBQzNELE9BQU8sS0FBSyxFQUFFLFNBQVMsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBRWpFLE9BQU8sS0FBSyxFQUFFLFlBQVksRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzVELE9BQU8sRUFDTCxLQUFLLGdCQUFnQixFQUVyQixLQUFLLHlCQUF5QixFQUUvQixNQUFNLDBCQUEwQixDQUFDO0FBR2xDLE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLGNBQWMsQ0FBQztBQUVwRCxPQUFPLEtBQUssRUFBRSwwQkFBMEIsRUFBRSxNQUFNLFlBQVksQ0FBQztBQUU3RCxNQUFNLFdBQVcscUJBQXFCO0lBQ3BDLE9BQU8sRUFBRSxlQUFlLENBQUM7SUFDekIsWUFBWSxFQUFFLFlBQVksQ0FBQztDQUM1QjtBQUVEOzs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FrQkc7QUFDSCxxQkFBYSxpQkFBaUI7SUFVMUIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxNQUFNO0lBVHpCLE9BQU8sQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFTO0lBQzdCLE9BQU8sQ0FBQyxRQUFRLENBQUMsa0JBQWtCLENBQTRCO0lBQy9ELE9BQU8sQ0FBQyxRQUFRLENBQUMsYUFBYSxDQUFhO0lBRTNDLE9BQU8sQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFlO0lBQzVDLE9BQU8sQ0FBQyxRQUFRLENBQUMsT0FBTyxDQUFrQjtJQUUxQyxZQUNFLEVBQUUsRUFBRSwwQkFBMEIsRUFDYixNQUFNLEVBQUUsZ0JBQWdCLEVBQ3pDLElBQUksRUFBRSxxQkFBcUIsRUFtQjVCO0lBRUQ7Ozs7Ozs7Ozs7Ozs7Ozs7T0FnQkc7SUFDRyxrQkFBa0IsQ0FDdEIsZ0JBQWdCLEVBQUUsVUFBVSxFQUM1QixXQUFXLEVBQUUsUUFBUSxFQUNyQixPQUFPLEVBQUUseUJBQXlCLEVBQ2xDLE1BQU0sRUFBRSxDQUFDLFdBQVcsRUFBRSxRQUFRLEtBQUssT0FBTyxDQUFDLFNBQVMsQ0FBQyxHQUNwRCxPQUFPLENBQUMsU0FBUyxDQUFDLENBdURwQjtJQUVEOztPQUVHO0lBQ0gsSUFBSSxNQUFNLElBQUksTUFBTSxDQUVuQjtJQUVEOzs7T0FHRztJQUNHLEtBQUssa0JBRVY7SUFFRDs7O09BR0c7SUFDRyxJQUFJLGtCQUdUO0NBQ0YifQ==
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator_ha_signer.d.ts","sourceRoot":"","sources":["../src/validator_ha_signer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAEjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EACL,KAAK,gBAAgB,EAErB,KAAK,yBAAyB,EAE/B,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAE7D,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,eAAe,CAAC;IACzB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,iBAAiB;IAU1B,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA4B;IAC/D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAa;IAE3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkB;IAE1C,YACE,EAAE,EAAE,0BAA0B,EACb,MAAM,EAAE,gBAAgB,EACzC,IAAI,EAAE,qBAAqB,EAmB5B;IAED;;;;;;;;;;;;;;;;OAgBG;IACG,kBAAkB,CACtB,gBAAgB,EAAE,UAAU,EAC5B,WAAW,EAAE,QAAQ,EACrB,OAAO,EAAE,yBAAyB,EAClC,MAAM,EAAE,CAAC,WAAW,EAAE,QAAQ,KAAK,OAAO,CAAC,SAAS,CAAC,GACpD,OAAO,CAAC,SAAS,CAAC,CAuDpB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED;;;OAGG;IACG,KAAK,kBAEV;IAED;;;OAGG;IACG,IAAI,kBAGT;CACF"}
@@ -0,0 +1,140 @@
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
+ */ import { createLogger } from '@aztec/foundation/log';
8
+ import { DutyType, getBlockNumberFromSigningContext } from '@aztec/stdlib/ha-signing';
9
+ import { SlashingProtectionService } from './slashing_protection_service.js';
10
+ /**
11
+ * Validator High Availability Signer
12
+ *
13
+ * Provides signing capabilities with distributed locking for validators
14
+ * in a high-availability setup.
15
+ *
16
+ * Usage:
17
+ * ```
18
+ * const signer = new ValidatorHASigner(db, config);
19
+ *
20
+ * // Sign with slashing protection
21
+ * const signature = await signer.signWithProtection(
22
+ * validatorAddress,
23
+ * messageHash,
24
+ * { slot: 100n, blockNumber: 50n, dutyType: 'BLOCK_PROPOSAL' },
25
+ * async (root) => localSigner.signMessage(root),
26
+ * );
27
+ * ```
28
+ */ export class ValidatorHASigner {
29
+ config;
30
+ log;
31
+ slashingProtection;
32
+ rollupAddress;
33
+ dateProvider;
34
+ metrics;
35
+ constructor(db, config, deps){
36
+ this.config = config;
37
+ this.log = createLogger('validator-ha-signer');
38
+ this.metrics = deps.metrics;
39
+ this.dateProvider = deps.dateProvider;
40
+ if (!config.nodeId || config.nodeId === '') {
41
+ throw new Error('NODE_ID is required for high-availability setups');
42
+ }
43
+ this.rollupAddress = config.l1Contracts.rollupAddress;
44
+ this.slashingProtection = new SlashingProtectionService(db, config, {
45
+ metrics: deps.metrics,
46
+ dateProvider: deps.dateProvider
47
+ });
48
+ this.log.info('Validator HA Signer initialized with slashing protection', {
49
+ nodeId: config.nodeId,
50
+ rollupAddress: this.rollupAddress.toString()
51
+ });
52
+ }
53
+ /**
54
+ * Sign a message with slashing protection.
55
+ *
56
+ * This method:
57
+ * 1. Acquires a distributed lock for (validator, slot, dutyType)
58
+ * 2. Calls the provided signing function
59
+ * 3. Records the result (success or failure)
60
+ *
61
+ * @param validatorAddress - The validator's Ethereum address
62
+ * @param messageHash - The hash to be signed
63
+ * @param context - The signing context (HA-protected duty types only)
64
+ * @param signFn - Function that performs the actual signing
65
+ * @returns The signature
66
+ *
67
+ * @throws DutyAlreadySignedError if the duty was already signed (expected in HA)
68
+ * @throws SlashingProtectionError if attempting to sign different data for same slot (expected in HA)
69
+ */ async signWithProtection(validatorAddress, messageHash, context, signFn) {
70
+ const startTime = this.dateProvider.now();
71
+ const dutyType = context.dutyType;
72
+ let dutyIdentifier;
73
+ if (context.dutyType === DutyType.BLOCK_PROPOSAL) {
74
+ dutyIdentifier = {
75
+ rollupAddress: this.rollupAddress,
76
+ validatorAddress,
77
+ slot: context.slot,
78
+ blockIndexWithinCheckpoint: context.blockIndexWithinCheckpoint,
79
+ dutyType: context.dutyType
80
+ };
81
+ } else {
82
+ dutyIdentifier = {
83
+ rollupAddress: this.rollupAddress,
84
+ validatorAddress,
85
+ slot: context.slot,
86
+ dutyType: context.dutyType
87
+ };
88
+ }
89
+ // Acquire lock and get the token for ownership verification
90
+ // DutyAlreadySignedError and SlashingProtectionError may be thrown here and are recorded in the service
91
+ const blockNumber = getBlockNumberFromSigningContext(context);
92
+ const lockToken = await this.slashingProtection.checkAndRecord({
93
+ ...dutyIdentifier,
94
+ blockNumber,
95
+ messageHash: messageHash.toString(),
96
+ nodeId: this.config.nodeId
97
+ });
98
+ // Perform signing
99
+ let signature;
100
+ try {
101
+ signature = await signFn(messageHash);
102
+ } catch (error) {
103
+ // Delete duty to allow retry (only succeeds if we own the lock)
104
+ await this.slashingProtection.deleteDuty({
105
+ ...dutyIdentifier,
106
+ lockToken
107
+ });
108
+ this.metrics.recordSigningError(dutyType);
109
+ throw error;
110
+ }
111
+ // Record success (only succeeds if we own the lock)
112
+ await this.slashingProtection.recordSuccess({
113
+ ...dutyIdentifier,
114
+ signature,
115
+ nodeId: this.config.nodeId,
116
+ lockToken
117
+ });
118
+ const duration = this.dateProvider.now() - startTime;
119
+ this.metrics.recordSigningSuccess(dutyType, duration);
120
+ return signature;
121
+ }
122
+ /**
123
+ * Get the node ID for this signer
124
+ */ get nodeId() {
125
+ return this.config.nodeId;
126
+ }
127
+ /**
128
+ * Start the HA signer background tasks (cleanup of stuck duties).
129
+ * Should be called after construction and before signing operations.
130
+ */ async start() {
131
+ await this.slashingProtection.start();
132
+ }
133
+ /**
134
+ * Stop the HA signer background tasks and close database connection.
135
+ * Should be called during graceful shutdown.
136
+ */ async stop() {
137
+ await this.slashingProtection.stop();
138
+ await this.slashingProtection.close();
139
+ }
140
+ }
package/package.json ADDED
@@ -0,0 +1,110 @@
1
+ {
2
+ "name": "@aztec/validator-ha-signer",
3
+ "version": "0.0.1-commit.001888fc",
4
+ "type": "module",
5
+ "exports": {
6
+ "./db": "./dest/db/index.js",
7
+ "./errors": "./dest/errors.js",
8
+ "./factory": "./dest/factory.js",
9
+ "./metrics": "./dest/metrics.js",
10
+ "./migrations": "./dest/migrations.js",
11
+ "./slashing-protection-service": "./dest/slashing_protection_service.js",
12
+ "./types": "./dest/types.js",
13
+ "./validator-ha-signer": "./dest/validator_ha_signer.js",
14
+ "./test": "./dest/test/pglite_pool.js",
15
+ "./db/lmdb": "./dest/db/lmdb.js"
16
+ },
17
+ "typedocOptions": {
18
+ "entryPoints": [
19
+ "./src/db/index.ts",
20
+ "./src/errors.ts",
21
+ "./src/factory.ts",
22
+ "./src/metrics.ts",
23
+ "./src/migrations.ts",
24
+ "./src/slashing_protection_service.ts",
25
+ "./src/types.ts",
26
+ "./src/validator_ha_signer.ts"
27
+ ],
28
+ "name": "Validator High-Availability Signer",
29
+ "tsconfig": "./tsconfig.json"
30
+ },
31
+ "scripts": {
32
+ "build": "yarn clean && ../scripts/tsc.sh",
33
+ "build:dev": "../scripts/tsc.sh --watch",
34
+ "clean": "rm -rf ./dest .tsbuildinfo",
35
+ "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
36
+ },
37
+ "inherits": [
38
+ "../package.common.json"
39
+ ],
40
+ "jest": {
41
+ "moduleNameMapper": {
42
+ "^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
43
+ },
44
+ "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
45
+ "rootDir": "./src",
46
+ "transform": {
47
+ "^.+\\.tsx?$": [
48
+ "@swc/jest",
49
+ {
50
+ "jsc": {
51
+ "parser": {
52
+ "syntax": "typescript",
53
+ "decorators": true
54
+ },
55
+ "transform": {
56
+ "decoratorVersion": "2022-03"
57
+ }
58
+ }
59
+ }
60
+ ]
61
+ },
62
+ "extensionsToTreatAsEsm": [
63
+ ".ts"
64
+ ],
65
+ "reporters": [
66
+ "default"
67
+ ],
68
+ "testTimeout": 120000,
69
+ "setupFiles": [
70
+ "../../foundation/src/jest/setup.mjs"
71
+ ],
72
+ "testEnvironment": "../../foundation/src/jest/env.mjs",
73
+ "setupFilesAfterEnv": [
74
+ "../../foundation/src/jest/setupAfterEnv.mjs"
75
+ ]
76
+ },
77
+ "dependencies": {
78
+ "@aztec/ethereum": "0.0.1-commit.001888fc",
79
+ "@aztec/foundation": "0.0.1-commit.001888fc",
80
+ "@aztec/kv-store": "0.0.1-commit.001888fc",
81
+ "@aztec/stdlib": "0.0.1-commit.001888fc",
82
+ "@aztec/telemetry-client": "0.0.1-commit.001888fc",
83
+ "node-pg-migrate": "^8.0.4",
84
+ "pg": "^8.11.3",
85
+ "tslib": "^2.4.0",
86
+ "zod": "^3.23.8"
87
+ },
88
+ "devDependencies": {
89
+ "@electric-sql/pglite": "^0.3.14",
90
+ "@jest/globals": "^30.0.0",
91
+ "@types/jest": "^30.0.0",
92
+ "@types/node": "^22.15.17",
93
+ "@types/node-pg-migrate": "^2.3.1",
94
+ "@types/pg": "^8.10.9",
95
+ "@typescript/native-preview": "7.0.0-dev.20260113.1",
96
+ "jest": "^30.0.0",
97
+ "jest-mock-extended": "^4.0.0",
98
+ "ts-node": "^10.9.1",
99
+ "typescript": "^5.3.3"
100
+ },
101
+ "types": "./dest/types.d.ts",
102
+ "files": [
103
+ "dest",
104
+ "src",
105
+ "!*.test.*"
106
+ ],
107
+ "engines": {
108
+ "node": ">=20.10"
109
+ }
110
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './schema.js';
3
+ export * from './postgres.js';
4
+ export * from './lmdb.js';