@aztec/validator-ha-signer 0.0.1-commit.ffe5b04ea → 0.0.1-commit.fff30aa
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 +2 -0
- package/dest/config.d.ts +101 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +92 -0
- package/dest/db/in_memory.d.ts +20 -0
- package/dest/db/in_memory.d.ts.map +1 -0
- package/dest/db/in_memory.js +73 -0
- package/dest/db/index.d.ts +1 -2
- package/dest/db/index.d.ts.map +1 -1
- package/dest/db/index.js +0 -1
- package/dest/db/postgres.d.ts +2 -4
- package/dest/db/postgres.d.ts.map +1 -1
- package/dest/db/postgres.js +13 -13
- package/dest/db/types.d.ts +18 -37
- package/dest/db/types.d.ts.map +1 -1
- package/dest/db/types.js +15 -30
- package/dest/factory.d.ts +13 -18
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +22 -42
- package/dest/slashing_protection_service.d.ts +3 -12
- package/dest/slashing_protection_service.d.ts.map +1 -1
- package/dest/slashing_protection_service.js +6 -17
- package/dest/types.d.ts +70 -17
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +20 -3
- package/dest/validator_ha_signer.d.ts +4 -12
- package/dest/validator_ha_signer.d.ts.map +1 -1
- package/dest/validator_ha_signer.js +8 -16
- package/package.json +6 -10
- package/src/config.ts +149 -0
- package/src/db/in_memory.ts +107 -0
- package/src/db/index.ts +0 -1
- package/src/db/postgres.ts +11 -13
- package/src/db/types.ts +16 -61
- package/src/factory.ts +28 -53
- package/src/slashing_protection_service.ts +7 -28
- package/src/types.ts +104 -32
- package/src/validator_ha_signer.ts +12 -33
- package/dest/db/lmdb.d.ts +0 -66
- package/dest/db/lmdb.d.ts.map +0 -1
- package/dest/db/lmdb.js +0 -188
- package/dest/metrics.d.ts +0 -51
- package/dest/metrics.d.ts.map +0 -1
- package/dest/metrics.js +0 -103
- package/src/db/lmdb.ts +0 -264
- package/src/metrics.ts +0 -138
package/package.json
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-ha-signer",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.fff30aa",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
|
+
"./config": "./dest/config.js",
|
|
6
7
|
"./db": "./dest/db/index.js",
|
|
7
8
|
"./errors": "./dest/errors.js",
|
|
8
9
|
"./factory": "./dest/factory.js",
|
|
9
|
-
"./metrics": "./dest/metrics.js",
|
|
10
10
|
"./migrations": "./dest/migrations.js",
|
|
11
11
|
"./slashing-protection-service": "./dest/slashing_protection_service.js",
|
|
12
12
|
"./types": "./dest/types.js",
|
|
13
13
|
"./validator-ha-signer": "./dest/validator_ha_signer.js",
|
|
14
|
-
"./test": "./dest/test/pglite_pool.js"
|
|
15
|
-
"./db/lmdb": "./dest/db/lmdb.js"
|
|
14
|
+
"./test": "./dest/test/pglite_pool.js"
|
|
16
15
|
},
|
|
17
16
|
"typedocOptions": {
|
|
18
17
|
"entryPoints": [
|
|
18
|
+
"./src/config.ts",
|
|
19
19
|
"./src/db/index.ts",
|
|
20
20
|
"./src/errors.ts",
|
|
21
21
|
"./src/factory.ts",
|
|
22
|
-
"./src/metrics.ts",
|
|
23
22
|
"./src/migrations.ts",
|
|
24
23
|
"./src/slashing_protection_service.ts",
|
|
25
24
|
"./src/types.ts",
|
|
@@ -75,11 +74,8 @@
|
|
|
75
74
|
]
|
|
76
75
|
},
|
|
77
76
|
"dependencies": {
|
|
78
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
79
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
80
|
-
"@aztec/kv-store": "0.0.1-commit.ffe5b04ea",
|
|
81
|
-
"@aztec/stdlib": "0.0.1-commit.ffe5b04ea",
|
|
82
|
-
"@aztec/telemetry-client": "0.0.1-commit.ffe5b04ea",
|
|
77
|
+
"@aztec/ethereum": "0.0.1-commit.fff30aa",
|
|
78
|
+
"@aztec/foundation": "0.0.1-commit.fff30aa",
|
|
83
79
|
"node-pg-migrate": "^8.0.4",
|
|
84
80
|
"pg": "^8.11.3",
|
|
85
81
|
"tslib": "^2.4.0",
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
|
|
2
|
+
import {
|
|
3
|
+
type ConfigMappingsType,
|
|
4
|
+
booleanConfigHelper,
|
|
5
|
+
getConfigFromMappings,
|
|
6
|
+
getDefaultConfig,
|
|
7
|
+
numberConfigHelper,
|
|
8
|
+
optionalNumberConfigHelper,
|
|
9
|
+
} from '@aztec/foundation/config';
|
|
10
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
11
|
+
import type { ZodFor } from '@aztec/foundation/schemas';
|
|
12
|
+
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for the Validator HA Signer
|
|
17
|
+
*
|
|
18
|
+
* This config is used for distributed locking and slashing protection
|
|
19
|
+
* when running multiple validator nodes in a high-availability setup.
|
|
20
|
+
*/
|
|
21
|
+
export interface ValidatorHASignerConfig {
|
|
22
|
+
/** Whether HA signing / slashing protection is enabled */
|
|
23
|
+
haSigningEnabled: boolean;
|
|
24
|
+
/** L1 contract addresses (rollup address required) */
|
|
25
|
+
l1Contracts: Pick<L1ContractAddresses, 'rollupAddress'>;
|
|
26
|
+
/** Unique identifier for this node */
|
|
27
|
+
nodeId: string;
|
|
28
|
+
/** How long to wait between polls when a duty is being signed (ms) */
|
|
29
|
+
pollingIntervalMs: number;
|
|
30
|
+
/** Maximum time to wait for a duty being signed to complete (ms) */
|
|
31
|
+
signingTimeoutMs: number;
|
|
32
|
+
/** Maximum age of a stuck duty in ms (defaults to 2x hardcoded Aztec slot duration if not set) */
|
|
33
|
+
maxStuckDutiesAgeMs?: number;
|
|
34
|
+
/** Optional: clean up old duties after this many hours (disabled if not set) */
|
|
35
|
+
cleanupOldDutiesAfterHours?: number;
|
|
36
|
+
/**
|
|
37
|
+
* PostgreSQL connection string
|
|
38
|
+
* Format: postgresql://user:password@host:port/database
|
|
39
|
+
*/
|
|
40
|
+
databaseUrl?: string;
|
|
41
|
+
/**
|
|
42
|
+
* PostgreSQL connection pool configuration
|
|
43
|
+
*/
|
|
44
|
+
/** Maximum number of clients in the pool (default: 10) */
|
|
45
|
+
poolMaxCount?: number;
|
|
46
|
+
/** Minimum number of clients in the pool (default: 0) */
|
|
47
|
+
poolMinCount?: number;
|
|
48
|
+
/** Idle timeout in milliseconds (default: 10000) */
|
|
49
|
+
poolIdleTimeoutMs?: number;
|
|
50
|
+
/** Connection timeout in milliseconds (default: 0, no timeout) */
|
|
51
|
+
poolConnectionTimeoutMs?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const validatorHASignerConfigMappings: ConfigMappingsType<ValidatorHASignerConfig> = {
|
|
55
|
+
haSigningEnabled: {
|
|
56
|
+
env: 'VALIDATOR_HA_SIGNING_ENABLED',
|
|
57
|
+
description: 'Whether HA signing / slashing protection is enabled',
|
|
58
|
+
...booleanConfigHelper(false),
|
|
59
|
+
},
|
|
60
|
+
l1Contracts: {
|
|
61
|
+
description: 'L1 contract addresses (rollup address required)',
|
|
62
|
+
nested: {
|
|
63
|
+
rollupAddress: {
|
|
64
|
+
description: 'The Ethereum address of the rollup contract (must be set programmatically)',
|
|
65
|
+
parseEnv: (val: string) => EthAddress.fromString(val),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
nodeId: {
|
|
70
|
+
env: 'VALIDATOR_HA_NODE_ID',
|
|
71
|
+
description: 'The unique identifier for this node',
|
|
72
|
+
defaultValue: '',
|
|
73
|
+
},
|
|
74
|
+
pollingIntervalMs: {
|
|
75
|
+
env: 'VALIDATOR_HA_POLLING_INTERVAL_MS',
|
|
76
|
+
description: 'The number of ms to wait between polls when a duty is being signed',
|
|
77
|
+
...numberConfigHelper(100),
|
|
78
|
+
},
|
|
79
|
+
signingTimeoutMs: {
|
|
80
|
+
env: 'VALIDATOR_HA_SIGNING_TIMEOUT_MS',
|
|
81
|
+
description: 'The maximum time to wait for a duty being signed to complete',
|
|
82
|
+
...numberConfigHelper(3_000),
|
|
83
|
+
},
|
|
84
|
+
maxStuckDutiesAgeMs: {
|
|
85
|
+
env: 'VALIDATOR_HA_MAX_STUCK_DUTIES_AGE_MS',
|
|
86
|
+
description: 'The maximum age of a stuck duty in ms (defaults to 2x Aztec slot duration)',
|
|
87
|
+
...optionalNumberConfigHelper(),
|
|
88
|
+
},
|
|
89
|
+
cleanupOldDutiesAfterHours: {
|
|
90
|
+
env: 'VALIDATOR_HA_OLD_DUTIES_MAX_AGE_H',
|
|
91
|
+
description: 'Optional: clean up old duties after this many hours (disabled if not set)',
|
|
92
|
+
...optionalNumberConfigHelper(),
|
|
93
|
+
},
|
|
94
|
+
databaseUrl: {
|
|
95
|
+
env: 'VALIDATOR_HA_DATABASE_URL',
|
|
96
|
+
description:
|
|
97
|
+
'PostgreSQL connection string for validator HA signer (format: postgresql://user:password@host:port/database)',
|
|
98
|
+
},
|
|
99
|
+
poolMaxCount: {
|
|
100
|
+
env: 'VALIDATOR_HA_POOL_MAX',
|
|
101
|
+
description: 'Maximum number of clients in the pool',
|
|
102
|
+
...numberConfigHelper(10),
|
|
103
|
+
},
|
|
104
|
+
poolMinCount: {
|
|
105
|
+
env: 'VALIDATOR_HA_POOL_MIN',
|
|
106
|
+
description: 'Minimum number of clients in the pool',
|
|
107
|
+
...numberConfigHelper(0),
|
|
108
|
+
},
|
|
109
|
+
poolIdleTimeoutMs: {
|
|
110
|
+
env: 'VALIDATOR_HA_POOL_IDLE_TIMEOUT_MS',
|
|
111
|
+
description: 'Idle timeout in milliseconds',
|
|
112
|
+
...numberConfigHelper(10_000),
|
|
113
|
+
},
|
|
114
|
+
poolConnectionTimeoutMs: {
|
|
115
|
+
env: 'VALIDATOR_HA_POOL_CONNECTION_TIMEOUT_MS',
|
|
116
|
+
description: 'Connection timeout in milliseconds (0 means no timeout)',
|
|
117
|
+
...numberConfigHelper(0),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const defaultValidatorHASignerConfig: ValidatorHASignerConfig = getDefaultConfig(
|
|
122
|
+
validatorHASignerConfigMappings,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns the validator HA signer configuration from environment variables.
|
|
127
|
+
* Note: If an environment variable is not set, the default value is used.
|
|
128
|
+
* @returns The validator HA signer configuration.
|
|
129
|
+
*/
|
|
130
|
+
export function getConfigEnvVars(): ValidatorHASignerConfig {
|
|
131
|
+
return getConfigFromMappings<ValidatorHASignerConfig>(validatorHASignerConfigMappings);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const ValidatorHASignerConfigSchema = z.object({
|
|
135
|
+
haSigningEnabled: z.boolean(),
|
|
136
|
+
l1Contracts: z.object({
|
|
137
|
+
rollupAddress: z.instanceof(EthAddress),
|
|
138
|
+
}),
|
|
139
|
+
nodeId: z.string(),
|
|
140
|
+
pollingIntervalMs: z.number().min(0),
|
|
141
|
+
signingTimeoutMs: z.number().min(0),
|
|
142
|
+
maxStuckDutiesAgeMs: z.number().min(0).optional(),
|
|
143
|
+
cleanupOldDutiesAfterHours: z.number().min(0).optional(),
|
|
144
|
+
databaseUrl: z.string().optional(),
|
|
145
|
+
poolMaxCount: z.number().min(0).optional(),
|
|
146
|
+
poolMinCount: z.number().min(0).optional(),
|
|
147
|
+
poolIdleTimeoutMs: z.number().min(0).optional(),
|
|
148
|
+
poolConnectionTimeoutMs: z.number().min(0).optional(),
|
|
149
|
+
}) satisfies ZodFor<ValidatorHASignerConfig>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory implementation of SlashingProtectionDatabase for testing.
|
|
3
|
+
* Used to simulate shared slashing protection in HA test setups without requiring PostgreSQL.
|
|
4
|
+
*/
|
|
5
|
+
import type { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
6
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
|
+
|
|
8
|
+
import type { SlashingProtectionDatabase, TryInsertOrGetResult, ValidatorDutyRecord } from '../types.js';
|
|
9
|
+
import type { CheckAndRecordParams, DutyType } from './types.js';
|
|
10
|
+
import { DutyStatus, getBlockIndexFromDutyIdentifier } from './types.js';
|
|
11
|
+
|
|
12
|
+
/** Creates a unique key for a duty based on its identifying fields. */
|
|
13
|
+
function dutyKey(
|
|
14
|
+
rollupAddress: EthAddress,
|
|
15
|
+
validatorAddress: EthAddress,
|
|
16
|
+
slot: SlotNumber,
|
|
17
|
+
dutyType: DutyType,
|
|
18
|
+
blockIndexWithinCheckpoint: number,
|
|
19
|
+
): string {
|
|
20
|
+
return `${rollupAddress}:${validatorAddress}:${slot}:${dutyType}:${blockIndexWithinCheckpoint}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** In-memory slashing protection database for testing HA setups. */
|
|
24
|
+
export class InMemorySlashingProtectionDatabase implements SlashingProtectionDatabase {
|
|
25
|
+
private duties = new Map<string, ValidatorDutyRecord>();
|
|
26
|
+
|
|
27
|
+
tryInsertOrGetExisting(params: CheckAndRecordParams): Promise<TryInsertOrGetResult> {
|
|
28
|
+
const blockIndex = getBlockIndexFromDutyIdentifier(params);
|
|
29
|
+
const key = dutyKey(params.rollupAddress, params.validatorAddress, params.slot, params.dutyType, blockIndex);
|
|
30
|
+
|
|
31
|
+
const existing = this.duties.get(key);
|
|
32
|
+
if (existing) {
|
|
33
|
+
return Promise.resolve({ isNew: false, record: existing });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const lockToken = `lock-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
37
|
+
const record: ValidatorDutyRecord = {
|
|
38
|
+
rollupAddress: params.rollupAddress,
|
|
39
|
+
validatorAddress: params.validatorAddress,
|
|
40
|
+
slot: params.slot,
|
|
41
|
+
blockNumber: params.blockNumber as BlockNumber,
|
|
42
|
+
blockIndexWithinCheckpoint: blockIndex,
|
|
43
|
+
dutyType: params.dutyType,
|
|
44
|
+
status: DutyStatus.SIGNING,
|
|
45
|
+
messageHash: params.messageHash,
|
|
46
|
+
nodeId: params.nodeId,
|
|
47
|
+
lockToken,
|
|
48
|
+
startedAt: new Date(),
|
|
49
|
+
};
|
|
50
|
+
this.duties.set(key, record);
|
|
51
|
+
return Promise.resolve({ isNew: true, record });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
updateDutySigned(
|
|
55
|
+
rollupAddress: EthAddress,
|
|
56
|
+
validatorAddress: EthAddress,
|
|
57
|
+
slot: SlotNumber,
|
|
58
|
+
dutyType: DutyType,
|
|
59
|
+
signature: string,
|
|
60
|
+
lockToken: string,
|
|
61
|
+
blockIndexWithinCheckpoint: number,
|
|
62
|
+
): Promise<boolean> {
|
|
63
|
+
const key = dutyKey(rollupAddress, validatorAddress, slot, dutyType, blockIndexWithinCheckpoint);
|
|
64
|
+
const record = this.duties.get(key);
|
|
65
|
+
if (!record || record.lockToken !== lockToken) {
|
|
66
|
+
return Promise.resolve(false);
|
|
67
|
+
}
|
|
68
|
+
record.status = DutyStatus.SIGNED;
|
|
69
|
+
record.signature = signature;
|
|
70
|
+
record.completedAt = new Date();
|
|
71
|
+
return Promise.resolve(true);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
deleteDuty(
|
|
75
|
+
rollupAddress: EthAddress,
|
|
76
|
+
validatorAddress: EthAddress,
|
|
77
|
+
slot: SlotNumber,
|
|
78
|
+
dutyType: DutyType,
|
|
79
|
+
lockToken: string,
|
|
80
|
+
blockIndexWithinCheckpoint: number,
|
|
81
|
+
): Promise<boolean> {
|
|
82
|
+
const key = dutyKey(rollupAddress, validatorAddress, slot, dutyType, blockIndexWithinCheckpoint);
|
|
83
|
+
const record = this.duties.get(key);
|
|
84
|
+
if (!record || record.lockToken !== lockToken) {
|
|
85
|
+
return Promise.resolve(false);
|
|
86
|
+
}
|
|
87
|
+
this.duties.delete(key);
|
|
88
|
+
return Promise.resolve(true);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cleanupOwnStuckDuties(_nodeId: string, _maxAgeMs: number): Promise<number> {
|
|
92
|
+
return Promise.resolve(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
cleanupOutdatedRollupDuties(_currentRollupAddress: EthAddress): Promise<number> {
|
|
96
|
+
return Promise.resolve(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
cleanupOldDuties(_maxAgeMs: number): Promise<number> {
|
|
100
|
+
return Promise.resolve(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
close(): Promise<void> {
|
|
104
|
+
this.duties.clear();
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/db/index.ts
CHANGED
package/src/db/postgres.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* PostgreSQL implementation of SlashingProtectionDatabase
|
|
3
3
|
*/
|
|
4
|
-
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
|
+
import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
5
|
import { randomBytes } from '@aztec/foundation/crypto/random';
|
|
6
6
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
7
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
UPDATE_DUTY_SIGNED,
|
|
21
21
|
} from './schema.js';
|
|
22
22
|
import type { CheckAndRecordParams, DutyRow, DutyType, InsertOrGetRow, ValidatorDutyRecord } from './types.js';
|
|
23
|
-
import { getBlockIndexFromDutyIdentifier
|
|
23
|
+
import { getBlockIndexFromDutyIdentifier } from './types.js';
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Minimal pool interface for database operations.
|
|
@@ -220,16 +220,14 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
|
-
* Convert a database row to a ValidatorDutyRecord
|
|
224
|
-
* Maps snake_case column names to StoredDutyRecord (camelCase, ms timestamps),
|
|
225
|
-
* then delegates to the shared recordFromFields() converter.
|
|
223
|
+
* Convert a database row to a ValidatorDutyRecord
|
|
226
224
|
*/
|
|
227
225
|
private rowToRecord(row: DutyRow): ValidatorDutyRecord {
|
|
228
|
-
return
|
|
229
|
-
rollupAddress: row.rollup_address,
|
|
230
|
-
validatorAddress: row.validator_address,
|
|
231
|
-
slot: row.slot,
|
|
232
|
-
blockNumber: row.block_number,
|
|
226
|
+
return {
|
|
227
|
+
rollupAddress: EthAddress.fromString(row.rollup_address),
|
|
228
|
+
validatorAddress: EthAddress.fromString(row.validator_address),
|
|
229
|
+
slot: SlotNumber.fromString(row.slot),
|
|
230
|
+
blockNumber: BlockNumber.fromString(row.block_number),
|
|
233
231
|
blockIndexWithinCheckpoint: row.block_index_within_checkpoint,
|
|
234
232
|
dutyType: row.duty_type,
|
|
235
233
|
status: row.status,
|
|
@@ -237,10 +235,10 @@ export class PostgresSlashingProtectionDatabase implements SlashingProtectionDat
|
|
|
237
235
|
signature: row.signature ?? undefined,
|
|
238
236
|
nodeId: row.node_id,
|
|
239
237
|
lockToken: row.lock_token,
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
startedAt: row.started_at,
|
|
239
|
+
completedAt: row.completed_at ?? undefined,
|
|
242
240
|
errorMessage: row.error_message ?? undefined,
|
|
243
|
-
}
|
|
241
|
+
};
|
|
244
242
|
}
|
|
245
243
|
|
|
246
244
|
/**
|
package/src/db/types.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type CheckpointNumber,
|
|
4
|
-
type IndexWithinCheckpoint,
|
|
5
|
-
SlotNumber,
|
|
6
|
-
} from '@aztec/foundation/branded-types';
|
|
7
|
-
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
1
|
+
import type { BlockNumber, CheckpointNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
8
3
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
9
|
-
import { DutyType } from '@aztec/stdlib/ha-signing';
|
|
10
4
|
|
|
11
5
|
/**
|
|
12
6
|
* Row type from PostgreSQL query
|
|
@@ -29,34 +23,24 @@ export interface DutyRow {
|
|
|
29
23
|
}
|
|
30
24
|
|
|
31
25
|
/**
|
|
32
|
-
*
|
|
33
|
-
* (e.g. msgpackr for LMDB). All domain types are stored as their string/number
|
|
34
|
-
* equivalents. Timestamps are Unix milliseconds.
|
|
26
|
+
* Row type from INSERT_OR_GET_DUTY query (includes is_new flag)
|
|
35
27
|
*/
|
|
36
|
-
export interface
|
|
37
|
-
|
|
38
|
-
validatorAddress: string;
|
|
39
|
-
slot: string;
|
|
40
|
-
blockNumber: string;
|
|
41
|
-
blockIndexWithinCheckpoint: number;
|
|
42
|
-
dutyType: DutyType;
|
|
43
|
-
status: DutyStatus;
|
|
44
|
-
messageHash: string;
|
|
45
|
-
signature?: string;
|
|
46
|
-
nodeId: string;
|
|
47
|
-
lockToken: string;
|
|
48
|
-
/** Unix timestamp in milliseconds when signing started */
|
|
49
|
-
startedAtMs: number;
|
|
50
|
-
/** Unix timestamp in milliseconds when signing completed */
|
|
51
|
-
completedAtMs?: number;
|
|
52
|
-
errorMessage?: string;
|
|
28
|
+
export interface InsertOrGetRow extends DutyRow {
|
|
29
|
+
is_new: boolean;
|
|
53
30
|
}
|
|
54
31
|
|
|
55
32
|
/**
|
|
56
|
-
*
|
|
33
|
+
* Type of validator duty being performed
|
|
57
34
|
*/
|
|
58
|
-
export
|
|
59
|
-
|
|
35
|
+
export enum DutyType {
|
|
36
|
+
BLOCK_PROPOSAL = 'BLOCK_PROPOSAL',
|
|
37
|
+
CHECKPOINT_PROPOSAL = 'CHECKPOINT_PROPOSAL',
|
|
38
|
+
ATTESTATION = 'ATTESTATION',
|
|
39
|
+
ATTESTATIONS_AND_SIGNERS = 'ATTESTATIONS_AND_SIGNERS',
|
|
40
|
+
GOVERNANCE_VOTE = 'GOVERNANCE_VOTE',
|
|
41
|
+
SLASHING_VOTE = 'SLASHING_VOTE',
|
|
42
|
+
AUTH_REQUEST = 'AUTH_REQUEST',
|
|
43
|
+
TXS = 'TXS',
|
|
60
44
|
}
|
|
61
45
|
|
|
62
46
|
/**
|
|
@@ -67,12 +51,8 @@ export enum DutyStatus {
|
|
|
67
51
|
SIGNED = 'signed',
|
|
68
52
|
}
|
|
69
53
|
|
|
70
|
-
// Re-export DutyType from stdlib
|
|
71
|
-
export { DutyType };
|
|
72
|
-
|
|
73
54
|
/**
|
|
74
|
-
*
|
|
75
|
-
* This is the common output type returned by all SlashingProtectionDatabase implementations.
|
|
55
|
+
* Record of a validator duty in the database
|
|
76
56
|
*/
|
|
77
57
|
export interface ValidatorDutyRecord {
|
|
78
58
|
/** Ethereum address of the rollup contract */
|
|
@@ -105,31 +85,6 @@ export interface ValidatorDutyRecord {
|
|
|
105
85
|
errorMessage?: string;
|
|
106
86
|
}
|
|
107
87
|
|
|
108
|
-
/**
|
|
109
|
-
* Convert a {@link StoredDutyRecord} (plain-primitive wire format) to a
|
|
110
|
-
* {@link ValidatorDutyRecord} (rich domain type).
|
|
111
|
-
*
|
|
112
|
-
* Shared by LMDB and any future non-Postgres backend implementations.
|
|
113
|
-
*/
|
|
114
|
-
export function recordFromFields(stored: StoredDutyRecord): ValidatorDutyRecord {
|
|
115
|
-
return {
|
|
116
|
-
rollupAddress: EthAddress.fromString(stored.rollupAddress),
|
|
117
|
-
validatorAddress: EthAddress.fromString(stored.validatorAddress),
|
|
118
|
-
slot: SlotNumber.fromString(stored.slot),
|
|
119
|
-
blockNumber: BlockNumber.fromString(stored.blockNumber),
|
|
120
|
-
blockIndexWithinCheckpoint: stored.blockIndexWithinCheckpoint,
|
|
121
|
-
dutyType: stored.dutyType,
|
|
122
|
-
status: stored.status,
|
|
123
|
-
messageHash: stored.messageHash,
|
|
124
|
-
signature: stored.signature,
|
|
125
|
-
nodeId: stored.nodeId,
|
|
126
|
-
lockToken: stored.lockToken,
|
|
127
|
-
startedAt: new Date(stored.startedAtMs),
|
|
128
|
-
completedAt: stored.completedAtMs !== undefined ? new Date(stored.completedAtMs) : undefined,
|
|
129
|
-
errorMessage: stored.errorMessage,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
88
|
/**
|
|
134
89
|
* Duty identifier for block proposals.
|
|
135
90
|
* blockIndexWithinCheckpoint is REQUIRED and must be >= 0.
|
package/src/factory.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Factory functions for creating validator HA signers
|
|
3
3
|
*/
|
|
4
|
-
import { DateProvider } from '@aztec/foundation/timer';
|
|
5
|
-
import { createStore } from '@aztec/kv-store/lmdb-v2';
|
|
6
|
-
import type { LocalSignerConfig, ValidatorHASignerConfig } from '@aztec/stdlib/ha-signing';
|
|
7
|
-
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
8
|
-
|
|
9
4
|
import { Pool } from 'pg';
|
|
10
5
|
|
|
11
|
-
import {
|
|
6
|
+
import type { ValidatorHASignerConfig } from './config.js';
|
|
7
|
+
import { InMemorySlashingProtectionDatabase } from './db/in_memory.js';
|
|
12
8
|
import { PostgresSlashingProtectionDatabase } from './db/postgres.js';
|
|
13
|
-
import {
|
|
14
|
-
import type { CreateHASignerDeps, CreateLocalSignerWithProtectionDeps, SlashingProtectionDatabase } from './types.js';
|
|
9
|
+
import type { CreateHASignerDeps, SlashingProtectionDatabase } from './types.js';
|
|
15
10
|
import { ValidatorHASigner } from './validator_ha_signer.js';
|
|
16
11
|
|
|
17
12
|
/**
|
|
@@ -29,6 +24,7 @@ import { ValidatorHASigner } from './validator_ha_signer.js';
|
|
|
29
24
|
* ```typescript
|
|
30
25
|
* const { signer, db } = await createHASigner({
|
|
31
26
|
* databaseUrl: process.env.DATABASE_URL,
|
|
27
|
+
* haSigningEnabled: true,
|
|
32
28
|
* nodeId: 'validator-node-1',
|
|
33
29
|
* pollingIntervalMs: 100,
|
|
34
30
|
* signingTimeoutMs: 3000,
|
|
@@ -60,10 +56,6 @@ export async function createHASigner(
|
|
|
60
56
|
if (!databaseUrl) {
|
|
61
57
|
throw new Error('databaseUrl is required for createHASigner');
|
|
62
58
|
}
|
|
63
|
-
|
|
64
|
-
const telemetryClient = deps?.telemetryClient ?? getTelemetryClient();
|
|
65
|
-
const dateProvider = deps?.dateProvider ?? new DateProvider();
|
|
66
|
-
|
|
67
59
|
// Create connection pool (or use provided pool)
|
|
68
60
|
let pool: Pool;
|
|
69
61
|
if (!deps?.pool) {
|
|
@@ -84,56 +76,39 @@ export async function createHASigner(
|
|
|
84
76
|
// Verify database schema is initialized and version matches
|
|
85
77
|
await db.initialize();
|
|
86
78
|
|
|
87
|
-
// Create metrics
|
|
88
|
-
const metrics = new HASignerMetrics(telemetryClient, signerConfig.nodeId);
|
|
89
|
-
|
|
90
79
|
// Create signer
|
|
91
|
-
const signer = new ValidatorHASigner(db,
|
|
80
|
+
const signer = new ValidatorHASigner(db, { ...signerConfig, databaseUrl });
|
|
92
81
|
|
|
93
82
|
return { signer, db };
|
|
94
83
|
}
|
|
95
84
|
|
|
96
85
|
/**
|
|
97
|
-
* Create
|
|
98
|
-
*
|
|
99
|
-
* This provides double-signing protection for nodes that are NOT running in a
|
|
100
|
-
* high-availability (multi-node) setup. It prevents a proposer from sending two
|
|
101
|
-
* proposals for the same slot if the node crashes and restarts mid-proposal.
|
|
102
|
-
*
|
|
103
|
-
* When `config.dataDirectory` is set, the protection database is persisted to disk
|
|
104
|
-
* and survives crashes/restarts. When unset, an ephemeral in-memory store is
|
|
105
|
-
* used which protects within a single run but not across restarts.
|
|
106
|
-
*
|
|
107
|
-
* @param config - Local signer config
|
|
108
|
-
* @param deps - Optional dependencies (telemetry, date provider).
|
|
109
|
-
* @returns An object containing the signer and database instances.
|
|
86
|
+
* Create an in-memory SlashingProtectionDatabase that can be shared across
|
|
87
|
+
* multiple validator nodes in the same process. Used for testing HA setups.
|
|
110
88
|
*/
|
|
111
|
-
export
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
): Promise<{
|
|
115
|
-
signer: ValidatorHASigner;
|
|
116
|
-
db: SlashingProtectionDatabase;
|
|
117
|
-
}> {
|
|
118
|
-
const telemetryClient = deps?.telemetryClient ?? getTelemetryClient();
|
|
119
|
-
const dateProvider = deps?.dateProvider ?? new DateProvider();
|
|
89
|
+
export function createSharedSlashingProtectionDb(): SlashingProtectionDatabase {
|
|
90
|
+
return new InMemorySlashingProtectionDatabase();
|
|
91
|
+
}
|
|
120
92
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Create a ValidatorHASigner backed by a pre-existing SlashingProtectionDatabase.
|
|
95
|
+
* Used for testing HA setups where multiple nodes share the same protection database.
|
|
96
|
+
*/
|
|
97
|
+
export function createSignerFromSharedDb(
|
|
98
|
+
db: SlashingProtectionDatabase,
|
|
99
|
+
config: Pick<
|
|
100
|
+
ValidatorHASignerConfig,
|
|
101
|
+
'nodeId' | 'pollingIntervalMs' | 'signingTimeoutMs' | 'maxStuckDutiesAgeMs' | 'l1Contracts'
|
|
102
|
+
>,
|
|
103
|
+
): { signer: ValidatorHASigner; db: SlashingProtectionDatabase } {
|
|
104
|
+
const signerConfig: ValidatorHASignerConfig = {
|
|
105
|
+
haSigningEnabled: true,
|
|
124
106
|
l1Contracts: config.l1Contracts,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const signerConfig = {
|
|
130
|
-
...config,
|
|
131
|
-
nodeId: config.nodeId || 'local',
|
|
107
|
+
nodeId: config.nodeId || `shared-${Date.now()}`,
|
|
108
|
+
pollingIntervalMs: config.pollingIntervalMs ?? 100,
|
|
109
|
+
signingTimeoutMs: config.signingTimeoutMs ?? 3000,
|
|
110
|
+
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs,
|
|
132
111
|
};
|
|
133
|
-
|
|
134
|
-
const metrics = new HASignerMetrics(telemetryClient, signerConfig.nodeId, 'LocalSigningProtectionMetrics');
|
|
135
|
-
|
|
136
|
-
const signer = new ValidatorHASigner(db, signerConfig, { metrics, dateProvider });
|
|
137
|
-
|
|
112
|
+
const signer = new ValidatorHASigner(db, signerConfig);
|
|
138
113
|
return { signer, db };
|
|
139
114
|
}
|