@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.
- package/README.md +42 -37
- package/dest/config.d.ts +49 -17
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +28 -19
- package/dest/db/postgres.d.ts +10 -3
- package/dest/db/postgres.d.ts.map +1 -1
- package/dest/db/postgres.js +50 -19
- package/dest/db/schema.d.ts +11 -7
- package/dest/db/schema.d.ts.map +1 -1
- package/dest/db/schema.js +19 -7
- package/dest/db/types.d.ts +75 -23
- package/dest/db/types.d.ts.map +1 -1
- package/dest/db/types.js +34 -0
- package/dest/errors.d.ts +9 -5
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +7 -4
- package/dest/factory.d.ts +6 -14
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +6 -11
- package/dest/migrations.d.ts +1 -1
- package/dest/migrations.d.ts.map +1 -1
- package/dest/migrations.js +13 -2
- package/dest/slashing_protection_service.d.ts +9 -3
- package/dest/slashing_protection_service.d.ts.map +1 -1
- package/dest/slashing_protection_service.js +21 -9
- package/dest/types.d.ts +78 -14
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +21 -1
- package/dest/validator_ha_signer.d.ts +7 -11
- package/dest/validator_ha_signer.d.ts.map +1 -1
- package/dest/validator_ha_signer.js +25 -29
- package/package.json +6 -5
- package/src/config.ts +59 -50
- package/src/db/postgres.ts +57 -17
- package/src/db/schema.ts +19 -7
- package/src/db/types.ts +105 -21
- package/src/errors.ts +7 -2
- package/src/factory.ts +8 -13
- package/src/migrations.ts +17 -1
- package/src/slashing_protection_service.ts +46 -12
- package/src/types.ts +116 -19
- package/src/validator_ha_signer.ts +32 -39
package/src/db/postgres.ts
CHANGED
|
@@ -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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
//
|
|
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:
|
|
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:
|
|
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:
|
|
181
|
-
blockNumber:
|
|
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
|
-
|
|
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', $
|
|
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 = $
|
|
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 = $
|
|
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 = $
|
|
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:
|
|
60
|
+
slot: SlotNumber;
|
|
54
61
|
/** Block number for this duty */
|
|
55
|
-
blockNumber:
|
|
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
|
-
*
|
|
86
|
+
* Duty identifier for block proposals.
|
|
87
|
+
* blockIndexWithinCheckpoint is REQUIRED and must be >= 0.
|
|
78
88
|
*/
|
|
79
|
-
export interface
|
|
89
|
+
export interface BlockProposalDutyIdentifier {
|
|
80
90
|
validatorAddress: EthAddress;
|
|
81
|
-
slot:
|
|
82
|
-
|
|
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
|
-
*
|
|
98
|
+
* Duty identifier for non-block-proposal duties.
|
|
99
|
+
* blockIndexWithinCheckpoint is not applicable (internally stored as -1).
|
|
87
100
|
*/
|
|
88
|
-
export interface
|
|
101
|
+
export interface OtherDutyIdentifier {
|
|
89
102
|
validatorAddress: EthAddress;
|
|
90
|
-
slot:
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
170
|
+
* Parameters for checking and recording a new duty.
|
|
171
|
+
* Uses intersection with DutyIdentifier to preserve the discriminated union.
|
|
99
172
|
*/
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
185
|
+
* Parameters for recording a successful signing.
|
|
186
|
+
* Uses intersection with DutyIdentifier to preserve the discriminated union.
|
|
111
187
|
*/
|
|
112
|
-
export
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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:
|
|
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:
|
|
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) {
|