@aztec/validator-ha-signer 4.0.0-nightly.20260115 → 4.0.0-nightly.20260117

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 (42) hide show
  1. package/README.md +42 -37
  2. package/dest/config.d.ts +49 -17
  3. package/dest/config.d.ts.map +1 -1
  4. package/dest/config.js +28 -19
  5. package/dest/db/postgres.d.ts +10 -3
  6. package/dest/db/postgres.d.ts.map +1 -1
  7. package/dest/db/postgres.js +50 -19
  8. package/dest/db/schema.d.ts +11 -7
  9. package/dest/db/schema.d.ts.map +1 -1
  10. package/dest/db/schema.js +19 -7
  11. package/dest/db/types.d.ts +75 -23
  12. package/dest/db/types.d.ts.map +1 -1
  13. package/dest/db/types.js +34 -0
  14. package/dest/errors.d.ts +9 -5
  15. package/dest/errors.d.ts.map +1 -1
  16. package/dest/errors.js +7 -4
  17. package/dest/factory.d.ts +6 -14
  18. package/dest/factory.d.ts.map +1 -1
  19. package/dest/factory.js +6 -11
  20. package/dest/migrations.d.ts +1 -1
  21. package/dest/migrations.d.ts.map +1 -1
  22. package/dest/migrations.js +13 -2
  23. package/dest/slashing_protection_service.d.ts +9 -3
  24. package/dest/slashing_protection_service.d.ts.map +1 -1
  25. package/dest/slashing_protection_service.js +21 -9
  26. package/dest/types.d.ts +78 -14
  27. package/dest/types.d.ts.map +1 -1
  28. package/dest/types.js +21 -1
  29. package/dest/validator_ha_signer.d.ts +7 -11
  30. package/dest/validator_ha_signer.d.ts.map +1 -1
  31. package/dest/validator_ha_signer.js +25 -29
  32. package/package.json +6 -5
  33. package/src/config.ts +59 -50
  34. package/src/db/postgres.ts +57 -17
  35. package/src/db/schema.ts +19 -7
  36. package/src/db/types.ts +105 -21
  37. package/src/errors.ts +7 -2
  38. package/src/factory.ts +8 -13
  39. package/src/migrations.ts +17 -1
  40. package/src/slashing_protection_service.ts +46 -12
  41. package/src/types.ts +116 -19
  42. package/src/validator_ha_signer.ts +32 -39
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * PostgreSQL implementation of SlashingProtectionDatabase
3
3
  */
4
+ import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
5
  import { randomBytes } from '@aztec/foundation/crypto/random';
5
6
  import { EthAddress } from '@aztec/foundation/eth-address';
6
7
  import { type Logger, createLogger } from '@aztec/foundation/log';
8
+ import { makeBackoff, retry } from '@aztec/foundation/retry';
7
9
 
8
10
  import type { QueryResult, QueryResultRow } from 'pg';
9
11
 
@@ -16,6 +18,7 @@ import {
16
18
  UPDATE_DUTY_SIGNED,
17
19
  } from './schema.js';
18
20
  import type { CheckAndRecordParams, DutyRow, DutyType, InsertOrGetRow, ValidatorDutyRecord } from './types.js';
21
+ import { getBlockIndexFromDutyIdentifier } from './types.js';
19
22
 
20
23
  /**
21
24
  * Minimal pool interface for database operations.
@@ -57,13 +60,13 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
57
60
  dbVersion = result.rows[0].version;
58
61
  } catch {
59
62
  throw new Error(
60
- 'Database schema not initialized. Please run migrations first: aztec migrate up --database-url <url>',
63
+ 'Database schema not initialized. Please run migrations first: aztec migrate-ha-db up --database-url <url>',
61
64
  );
62
65
  }
63
66
 
64
67
  if (dbVersion < SCHEMA_VERSION) {
65
68
  throw new Error(
66
- `Database schema version ${dbVersion} is outdated (expected ${SCHEMA_VERSION}). Please run migrations: aztec migrate up --database-url <url>`,
69
+ `Database schema version ${dbVersion} is outdated (expected ${SCHEMA_VERSION}). Please run migrations: aztec migrate-ha-db up --database-url <url>`,
67
70
  );
68
71
  }
69
72
 
@@ -81,24 +84,54 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
81
84
  *
82
85
  * @returns { isNew: true, record } if we successfully inserted and acquired the lock
83
86
  * @returns { isNew: false, record } if a record already exists. lock_token is empty if the record already exists.
87
+ *
88
+ * Retries if no rows are returned, which can happen under high concurrency
89
+ * when another transaction just committed the row but it's not yet visible.
84
90
  */
85
91
  async tryInsertOrGetExisting(params: CheckAndRecordParams): Promise<TryInsertOrGetResult> {
86
92
  // create a token for ownership verification
87
93
  const lockToken = randomBytes(16).toString('hex');
88
94
 
89
- const result: QueryResult<InsertOrGetRow> = await this.pool.query(INSERT_OR_GET_DUTY, [
90
- params.validatorAddress.toString(),
91
- params.slot.toString(),
92
- params.blockNumber.toString(),
93
- params.dutyType,
94
- params.messageHash,
95
- params.nodeId,
96
- lockToken,
97
- ]);
95
+ // Use fast retries with custom backoff: 10ms, 20ms, 30ms (then stop)
96
+ const fastBackoff = makeBackoff([0.01, 0.02, 0.03]);
97
+
98
+ // Get the normalized block index using type-safe helper
99
+ const blockIndexWithinCheckpoint = getBlockIndexFromDutyIdentifier(params);
100
+
101
+ const result = await retry<QueryResult<InsertOrGetRow>>(
102
+ async () => {
103
+ const queryResult: QueryResult<InsertOrGetRow> = await this.pool.query(INSERT_OR_GET_DUTY, [
104
+ params.validatorAddress.toString(),
105
+ params.slot.toString(),
106
+ params.blockNumber.toString(),
107
+ blockIndexWithinCheckpoint,
108
+ params.dutyType,
109
+ params.messageHash,
110
+ params.nodeId,
111
+ lockToken,
112
+ ]);
113
+
114
+ // Throw error if no rows to trigger retry
115
+ if (queryResult.rows.length === 0) {
116
+ throw new Error('INSERT_OR_GET_DUTY returned no rows');
117
+ }
118
+
119
+ return queryResult;
120
+ },
121
+ `INSERT_OR_GET_DUTY for node ${params.nodeId}`,
122
+ fastBackoff,
123
+ this.log,
124
+ true,
125
+ );
98
126
 
99
127
  if (result.rows.length === 0) {
100
- // This shouldn't happen - the query always returns either the inserted or existing row
101
- throw new Error('INSERT_OR_GET_DUTY returned no rows');
128
+ // this should never happen as the retry function should throw if it still fails after retries
129
+ throw new Error('INSERT_OR_GET_DUTY returned no rows after retries');
130
+ }
131
+
132
+ if (result.rows.length > 1) {
133
+ // this should never happen if database constraints are correct (PRIMARY KEY should prevent duplicates)
134
+ throw new Error(`INSERT_OR_GET_DUTY returned ${result.rows.length} rows (expected exactly 1).`);
102
135
  }
103
136
 
104
137
  const row = result.rows[0];
@@ -116,16 +149,18 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
116
149
  */
117
150
  async updateDutySigned(
118
151
  validatorAddress: EthAddress,
119
- slot: bigint,
152
+ slot: SlotNumber,
120
153
  dutyType: DutyType,
121
154
  signature: string,
122
155
  lockToken: string,
156
+ blockIndexWithinCheckpoint: number,
123
157
  ): Promise<boolean> {
124
158
  const result = await this.pool.query(UPDATE_DUTY_SIGNED, [
125
159
  signature,
126
160
  validatorAddress.toString(),
127
161
  slot.toString(),
128
162
  dutyType,
163
+ blockIndexWithinCheckpoint,
129
164
  lockToken,
130
165
  ]);
131
166
 
@@ -134,6 +169,7 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
134
169
  validatorAddress: validatorAddress.toString(),
135
170
  slot: slot.toString(),
136
171
  dutyType,
172
+ blockIndexWithinCheckpoint,
137
173
  });
138
174
  return false;
139
175
  }
@@ -149,14 +185,16 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
149
185
  */
150
186
  async deleteDuty(
151
187
  validatorAddress: EthAddress,
152
- slot: bigint,
188
+ slot: SlotNumber,
153
189
  dutyType: DutyType,
154
190
  lockToken: string,
191
+ blockIndexWithinCheckpoint: number,
155
192
  ): Promise<boolean> {
156
193
  const result = await this.pool.query(DELETE_DUTY, [
157
194
  validatorAddress.toString(),
158
195
  slot.toString(),
159
196
  dutyType,
197
+ blockIndexWithinCheckpoint,
160
198
  lockToken,
161
199
  ]);
162
200
 
@@ -165,6 +203,7 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
165
203
  validatorAddress: validatorAddress.toString(),
166
204
  slot: slot.toString(),
167
205
  dutyType,
206
+ blockIndexWithinCheckpoint,
168
207
  });
169
208
  return false;
170
209
  }
@@ -177,8 +216,9 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
177
216
  private rowToRecord(row: DutyRow): ValidatorDutyRecord {
178
217
  return {
179
218
  validatorAddress: EthAddress.fromString(row.validator_address),
180
- slot: BigInt(row.slot),
181
- blockNumber: BigInt(row.block_number),
219
+ slot: SlotNumber.fromString(row.slot),
220
+ blockNumber: BlockNumber.fromString(row.block_number),
221
+ blockIndexWithinCheckpoint: row.block_index_within_checkpoint,
182
222
  dutyType: row.duty_type,
183
223
  status: row.status,
184
224
  messageHash: row.message_hash,
package/src/db/schema.ts CHANGED
@@ -19,7 +19,8 @@ CREATE TABLE IF NOT EXISTS validator_duties (
19
19
  validator_address VARCHAR(42) NOT NULL,
20
20
  slot BIGINT NOT NULL,
21
21
  block_number BIGINT NOT NULL,
22
- duty_type VARCHAR(30) NOT NULL CHECK (duty_type IN ('BLOCK_PROPOSAL', 'ATTESTATION', 'ATTESTATIONS_AND_SIGNERS')),
22
+ block_index_within_checkpoint INTEGER NOT NULL DEFAULT 0,
23
+ duty_type VARCHAR(30) NOT NULL CHECK (duty_type IN ('BLOCK_PROPOSAL', 'CHECKPOINT_PROPOSAL', 'ATTESTATION', 'ATTESTATIONS_AND_SIGNERS', 'GOVERNANCE_VOTE', 'SLASHING_VOTE')),
23
24
  status VARCHAR(20) NOT NULL CHECK (status IN ('signing', 'signed', 'failed')),
24
25
  message_hash VARCHAR(66) NOT NULL,
25
26
  signature VARCHAR(132),
@@ -29,7 +30,7 @@ CREATE TABLE IF NOT EXISTS validator_duties (
29
30
  completed_at TIMESTAMP,
30
31
  error_message TEXT,
31
32
 
32
- PRIMARY KEY (validator_address, slot, duty_type),
33
+ PRIMARY KEY (validator_address, slot, duty_type, block_index_within_checkpoint),
33
34
  CHECK (completed_at IS NULL OR completed_at >= started_at)
34
35
  );
35
36
  `;
@@ -92,6 +93,10 @@ SELECT version FROM schema_version ORDER BY version DESC LIMIT 1;
92
93
  * returns the existing record instead.
93
94
  *
94
95
  * Returns the record with an `is_new` flag indicating whether we inserted or got existing.
96
+ *
97
+ * Note: In high concurrency scenarios, if the INSERT conflicts and another transaction
98
+ * just committed the row, there's a small window where the SELECT might not see it yet.
99
+ * The application layer should retry if no rows are returned.
95
100
  */
96
101
  export const INSERT_OR_GET_DUTY = `
97
102
  WITH inserted AS (
@@ -99,18 +104,20 @@ WITH inserted AS (
99
104
  validator_address,
100
105
  slot,
101
106
  block_number,
107
+ block_index_within_checkpoint,
102
108
  duty_type,
103
109
  status,
104
110
  message_hash,
105
111
  node_id,
106
112
  lock_token,
107
113
  started_at
108
- ) VALUES ($1, $2, $3, $4, 'signing', $5, $6, $7, CURRENT_TIMESTAMP)
109
- ON CONFLICT (validator_address, slot, duty_type) DO NOTHING
114
+ ) VALUES ($1, $2, $3, $4, $5, 'signing', $6, $7, $8, CURRENT_TIMESTAMP)
115
+ ON CONFLICT (validator_address, slot, duty_type, block_index_within_checkpoint) DO NOTHING
110
116
  RETURNING
111
117
  validator_address,
112
118
  slot,
113
119
  block_number,
120
+ block_index_within_checkpoint,
114
121
  duty_type,
115
122
  status,
116
123
  message_hash,
@@ -128,6 +135,7 @@ SELECT
128
135
  validator_address,
129
136
  slot,
130
137
  block_number,
138
+ block_index_within_checkpoint,
131
139
  duty_type,
132
140
  status,
133
141
  message_hash,
@@ -141,7 +149,8 @@ SELECT
141
149
  FROM validator_duties
142
150
  WHERE validator_address = $1
143
151
  AND slot = $2
144
- AND duty_type = $4
152
+ AND duty_type = $5
153
+ AND block_index_within_checkpoint = $4
145
154
  AND NOT EXISTS (SELECT 1 FROM inserted);
146
155
  `;
147
156
 
@@ -156,8 +165,9 @@ SET status = 'signed',
156
165
  WHERE validator_address = $2
157
166
  AND slot = $3
158
167
  AND duty_type = $4
168
+ AND block_index_within_checkpoint = $5
159
169
  AND status = 'signing'
160
- AND lock_token = $5;
170
+ AND lock_token = $6;
161
171
  `;
162
172
 
163
173
  /**
@@ -169,8 +179,9 @@ DELETE FROM validator_duties
169
179
  WHERE validator_address = $1
170
180
  AND slot = $2
171
181
  AND duty_type = $3
182
+ AND block_index_within_checkpoint = $4
172
183
  AND status = 'signing'
173
- AND lock_token = $4;
184
+ AND lock_token = $5;
174
185
  `;
175
186
 
176
187
  /**
@@ -223,6 +234,7 @@ SELECT
223
234
  validator_address,
224
235
  slot,
225
236
  block_number,
237
+ block_index_within_checkpoint,
226
238
  duty_type,
227
239
  status,
228
240
  message_hash,
package/src/db/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
2
3
  import type { Signature } from '@aztec/foundation/eth-signature';
3
4
 
@@ -8,6 +9,7 @@ export interface DutyRow {
8
9
  validator_address: string;
9
10
  slot: string;
10
11
  block_number: string;
12
+ block_index_within_checkpoint: number;
11
13
  duty_type: DutyType;
12
14
  status: DutyStatus;
13
15
  message_hash: string;
@@ -31,8 +33,13 @@ export interface InsertOrGetRow extends DutyRow {
31
33
  */
32
34
  export enum DutyType {
33
35
  BLOCK_PROPOSAL = 'BLOCK_PROPOSAL',
36
+ CHECKPOINT_PROPOSAL = 'CHECKPOINT_PROPOSAL',
34
37
  ATTESTATION = 'ATTESTATION',
35
38
  ATTESTATIONS_AND_SIGNERS = 'ATTESTATIONS_AND_SIGNERS',
39
+ GOVERNANCE_VOTE = 'GOVERNANCE_VOTE',
40
+ SLASHING_VOTE = 'SLASHING_VOTE',
41
+ AUTH_REQUEST = 'AUTH_REQUEST',
42
+ TXS = 'TXS',
36
43
  }
37
44
 
38
45
  /**
@@ -50,9 +57,11 @@ export interface ValidatorDutyRecord {
50
57
  /** Ethereum address of the validator */
51
58
  validatorAddress: EthAddress;
52
59
  /** Slot number for this duty */
53
- slot: bigint;
60
+ slot: SlotNumber;
54
61
  /** Block number for this duty */
55
- blockNumber: bigint;
62
+ blockNumber: BlockNumber;
63
+ /** Block index within checkpoint (0, 1, 2... for block proposals, -1 for other duty types) */
64
+ blockIndexWithinCheckpoint: number;
56
65
  /** Type of duty being performed */
57
66
  dutyType: DutyType;
58
67
  /** Current status of the duty */
@@ -74,44 +83,119 @@ export interface ValidatorDutyRecord {
74
83
  }
75
84
 
76
85
  /**
77
- * Minimal info needed to identify a unique duty
86
+ * Duty identifier for block proposals.
87
+ * blockIndexWithinCheckpoint is REQUIRED and must be >= 0.
78
88
  */
79
- export interface DutyIdentifier {
89
+ export interface BlockProposalDutyIdentifier {
80
90
  validatorAddress: EthAddress;
81
- slot: bigint;
82
- dutyType: DutyType;
91
+ slot: SlotNumber;
92
+ /** Block index within checkpoint (0, 1, 2...). Required for block proposals. */
93
+ blockIndexWithinCheckpoint: number;
94
+ dutyType: DutyType.BLOCK_PROPOSAL;
83
95
  }
84
96
 
85
97
  /**
86
- * Parameters for checking and recording a new duty
98
+ * Duty identifier for non-block-proposal duties.
99
+ * blockIndexWithinCheckpoint is not applicable (internally stored as -1).
87
100
  */
88
- export interface CheckAndRecordParams {
101
+ export interface OtherDutyIdentifier {
89
102
  validatorAddress: EthAddress;
90
- slot: bigint;
91
- blockNumber: bigint;
92
- dutyType: DutyType;
103
+ slot: SlotNumber;
104
+ dutyType:
105
+ | DutyType.CHECKPOINT_PROPOSAL
106
+ | DutyType.ATTESTATION
107
+ | DutyType.ATTESTATIONS_AND_SIGNERS
108
+ | DutyType.GOVERNANCE_VOTE
109
+ | DutyType.SLASHING_VOTE
110
+ | DutyType.AUTH_REQUEST
111
+ | DutyType.TXS;
112
+ }
113
+
114
+ /**
115
+ * Minimal info needed to identify a unique duty.
116
+ * Uses discriminated union to enforce type safety:
117
+ * - BLOCK_PROPOSAL duties MUST have blockIndexWithinCheckpoint >= 0
118
+ * - Other duty types do NOT have blockIndexWithinCheckpoint (internally -1)
119
+ */
120
+ export type DutyIdentifier = BlockProposalDutyIdentifier | OtherDutyIdentifier;
121
+
122
+ /**
123
+ * Validates and normalizes the block index for a duty.
124
+ * - BLOCK_PROPOSAL: validates blockIndexWithinCheckpoint is provided and >= 0
125
+ * - Other duty types: always returns -1
126
+ *
127
+ * @throws Error if BLOCK_PROPOSAL is missing blockIndexWithinCheckpoint or has invalid value
128
+ */
129
+ export function normalizeBlockIndex(dutyType: DutyType, blockIndexWithinCheckpoint: number | undefined): number {
130
+ if (dutyType === DutyType.BLOCK_PROPOSAL) {
131
+ if (blockIndexWithinCheckpoint === undefined) {
132
+ throw new Error('BLOCK_PROPOSAL duties require blockIndexWithinCheckpoint to be specified');
133
+ }
134
+ if (blockIndexWithinCheckpoint < 0) {
135
+ throw new Error(
136
+ `BLOCK_PROPOSAL duties require blockIndexWithinCheckpoint >= 0, got ${blockIndexWithinCheckpoint}`,
137
+ );
138
+ }
139
+ return blockIndexWithinCheckpoint;
140
+ }
141
+ // For all other duty types, always use -1
142
+ return -1;
143
+ }
144
+
145
+ /**
146
+ * Gets the block index from a DutyIdentifier.
147
+ * - BLOCK_PROPOSAL: returns the blockIndexWithinCheckpoint
148
+ * - Other duty types: returns -1
149
+ */
150
+ export function getBlockIndexFromDutyIdentifier(duty: DutyIdentifier): number {
151
+ if (duty.dutyType === DutyType.BLOCK_PROPOSAL) {
152
+ return duty.blockIndexWithinCheckpoint;
153
+ }
154
+ return -1;
155
+ }
156
+
157
+ /**
158
+ * Additional parameters for checking and recording a new duty
159
+ */
160
+ interface CheckAndRecordExtra {
161
+ /** Block number for this duty */
162
+ blockNumber: BlockNumber | CheckpointNumber;
163
+ /** The signing root (hash) for this duty */
93
164
  messageHash: string;
165
+ /** Identifier for the node that acquired the lock */
94
166
  nodeId: string;
95
167
  }
96
168
 
97
169
  /**
98
- * Parameters for recording a successful signing
170
+ * Parameters for checking and recording a new duty.
171
+ * Uses intersection with DutyIdentifier to preserve the discriminated union.
99
172
  */
100
- export interface RecordSuccessParams {
101
- validatorAddress: EthAddress;
102
- slot: bigint;
103
- dutyType: DutyType;
173
+ export type CheckAndRecordParams = DutyIdentifier & CheckAndRecordExtra;
174
+
175
+ /**
176
+ * Additional parameters for recording a successful signing
177
+ */
178
+ interface RecordSuccessExtra {
104
179
  signature: Signature;
105
180
  nodeId: string;
106
181
  lockToken: string;
107
182
  }
108
183
 
109
184
  /**
110
- * Parameters for deleting a duty
185
+ * Parameters for recording a successful signing.
186
+ * Uses intersection with DutyIdentifier to preserve the discriminated union.
111
187
  */
112
- export interface DeleteDutyParams {
113
- validatorAddress: EthAddress;
114
- slot: bigint;
115
- dutyType: DutyType;
188
+ export type RecordSuccessParams = DutyIdentifier & RecordSuccessExtra;
189
+
190
+ /**
191
+ * Additional parameters for deleting a duty
192
+ */
193
+ interface DeleteDutyExtra {
116
194
  lockToken: string;
117
195
  }
196
+
197
+ /**
198
+ * Parameters for deleting a duty.
199
+ * Uses intersection with DutyIdentifier to preserve the discriminated union.
200
+ */
201
+ export type DeleteDutyParams = DutyIdentifier & DeleteDutyExtra;
package/src/errors.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Custom errors for the validator HA signer
3
3
  */
4
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
5
+
4
6
  import type { DutyType } from './db/types.js';
5
7
 
6
8
  /**
@@ -10,8 +12,9 @@ import type { DutyType } from './db/types.js';
10
12
  */
11
13
  export class DutyAlreadySignedError extends Error {
12
14
  constructor(
13
- public readonly slot: bigint,
15
+ public readonly slot: SlotNumber,
14
16
  public readonly dutyType: DutyType,
17
+ public readonly blockIndexWithinCheckpoint: number,
15
18
  public readonly signedByNode: string,
16
19
  ) {
17
20
  super(`Duty ${dutyType} for slot ${slot} already signed by node ${signedByNode}`);
@@ -28,10 +31,12 @@ export class DutyAlreadySignedError extends Error {
28
31
  */
29
32
  export class SlashingProtectionError extends Error {
30
33
  constructor(
31
- public readonly slot: bigint,
34
+ public readonly slot: SlotNumber,
32
35
  public readonly dutyType: DutyType,
36
+ public readonly blockIndexWithinCheckpoint: number,
33
37
  public readonly existingMessageHash: string,
34
38
  public readonly attemptedMessageHash: string,
39
+ public readonly signedByNode: string,
35
40
  ) {
36
41
  super(
37
42
  `Slashing protection: ${dutyType} for slot ${slot} was already signed with different data. ` +
package/src/factory.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { Pool } from 'pg';
5
5
 
6
- import type { CreateHASignerConfig } from './config.js';
6
+ import type { ValidatorHASignerConfig } from './config.js';
7
7
  import { PostgresSlashingProtectionDatabase } from './db/postgres.js';
8
8
  import type { CreateHASignerDeps, SlashingProtectionDatabase } from './types.js';
9
9
  import { ValidatorHASigner } from './validator_ha_signer.js';
@@ -23,7 +23,7 @@ import { ValidatorHASigner } from './validator_ha_signer.js';
23
23
  * ```typescript
24
24
  * const { signer, db } = await createHASigner({
25
25
  * databaseUrl: process.env.DATABASE_URL,
26
- * enabled: true,
26
+ * haSigningEnabled: true,
27
27
  * nodeId: 'validator-node-1',
28
28
  * pollingIntervalMs: 100,
29
29
  * signingTimeoutMs: 3000,
@@ -35,23 +35,15 @@ import { ValidatorHASigner } from './validator_ha_signer.js';
35
35
  * await signer.stop(); // On shutdown
36
36
  * ```
37
37
  *
38
- * Example with automatic migrations (simpler for dev/testing):
39
- * ```typescript
40
- * const { signer, db } = await createHASigner({
41
- * databaseUrl: process.env.DATABASE_URL,
42
- * enabled: true,
43
- * nodeId: 'validator-node-1',
44
- * runMigrations: true, // Auto-run migrations on startup
45
- * });
46
- * signer.start();
47
- * ```
38
+ * Note: Migrations must be run separately using `aztec migrate-ha-db up` before
39
+ * creating the signer. The factory will verify the schema is initialized via `db.initialize()`.
48
40
  *
49
41
  * @param config - Configuration for the HA signer
50
42
  * @param deps - Optional dependencies (e.g., for testing)
51
43
  * @returns An object containing the signer and database instances
52
44
  */
53
45
  export async function createHASigner(
54
- config: CreateHASignerConfig,
46
+ config: ValidatorHASignerConfig,
55
47
  deps?: CreateHASignerDeps,
56
48
  ): Promise<{
57
49
  signer: ValidatorHASigner;
@@ -60,6 +52,9 @@ export async function createHASigner(
60
52
  const { databaseUrl, poolMaxCount, poolMinCount, poolIdleTimeoutMs, poolConnectionTimeoutMs, ...signerConfig } =
61
53
  config;
62
54
 
55
+ if (!databaseUrl) {
56
+ throw new Error('databaseUrl is required for createHASigner');
57
+ }
63
58
  // Create connection pool (or use provided pool)
64
59
  let pool: Pool;
65
60
  if (!deps?.pool) {
package/src/migrations.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { createLogger } from '@aztec/foundation/log';
5
5
 
6
+ import { readdirSync } from 'fs';
6
7
  import { runner } from 'node-pg-migrate';
7
8
  import { dirname, join } from 'path';
8
9
  import { fileURLToPath } from 'url';
@@ -30,17 +31,32 @@ export async function runMigrations(databaseUrl: string, options: RunMigrationsO
30
31
 
31
32
  const log = createLogger('validator-ha-signer:migrations');
32
33
 
34
+ const migrationsDir = join(__dirname, 'db', 'migrations');
35
+
33
36
  try {
34
37
  log.info(`Running migrations ${direction}...`);
35
38
 
39
+ // Filter out .d.ts and .d.ts.map files - node-pg-migrate only needs .js files
40
+ const migrationFiles = readdirSync(migrationsDir);
41
+ const jsMigrationFiles = migrationFiles.filter(
42
+ file => file.endsWith('.js') && !file.endsWith('.d.ts') && !file.endsWith('.d.ts.map'),
43
+ );
44
+
45
+ if (jsMigrationFiles.length === 0) {
46
+ log.info('No migration files found');
47
+ return [];
48
+ }
49
+
36
50
  const appliedMigrations = await runner({
37
51
  databaseUrl,
38
- dir: join(__dirname, 'db', 'migrations'),
52
+ dir: migrationsDir,
39
53
  direction,
40
54
  migrationsTable: 'pgmigrations',
41
55
  count: direction === 'down' ? 1 : Infinity,
42
56
  verbose,
43
57
  log: msg => (verbose ? log.info(msg) : log.debug(msg)),
58
+ // Ignore TypeScript declaration files - node-pg-migrate will try to import them otherwise
59
+ ignorePattern: '.*\\.d\\.(ts|js)$|.*\\.d\\.ts\\.map$',
44
60
  });
45
61
 
46
62
  if (appliedMigrations.length === 0) {