Skip to content

Commit 2736f9b

Browse files
authored
[feat]: Overhaul private key management (#27)
* feat: Overhaul the 'privateKey' loading mechanism * chore: Minor code refactor * chore: Undo the removal of the wallet service 'publicKey' property * Merge branch 'testnet' into jsanmi/overhaul-private-key-management
1 parent 03a2e38 commit 2736f9b

9 files changed

+161
-23
lines changed

config.example.yaml

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
# Global Relayer configuration
22
global:
3-
privateKey: '' # The privateKey of the account that will be submitting the packet relays
3+
# ! The 'privateKey' of the account that will be submitting the packet relays is by default
4+
# ! loaded from the environment variable 'RELAYER_PRIVATE_KEY'. Alternatively, the privateKey
5+
# ! may be specified here (not recommended).
6+
# privateKey: ''
7+
# ! Optionally, custom privateKey loaders may be implemented and specified (NOTE: the 'env'
8+
# ! loader is used if no privateKey configuration is specified):
9+
# privateKey:
10+
# loader: 'env' # The privateKey loader name (must match the implementation on src/config/privateKeyLoaders/<loader>.ts).
11+
# customLoaderConfig: '' # Custom loader configs may be specified.
12+
413
logLevel: 'info'
514

615
monitor:

src/config/config.schema.ts

+23-7
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { AnyValidateFunction } from "ajv/dist/core"
44
const MIN_PROCESSING_INTERVAL = 1;
55
const MAX_PROCESSING_INTERVAL = 500;
66

7-
const EVM_ADDRESS_EXPR = '^0x[0-9a-fA-F]{40}$'; // '0x' + 20 bytes (40 chars)
8-
const BYTES_32_HEX_EXPR = '^0x[0-9a-fA-F]{64}$'; // '0x' + 32 bytes (64 chars)
7+
export const EVM_ADDRESS_EXPR = '^0x[0-9a-fA-F]{40}$'; // '0x' + 20 bytes (40 chars)
8+
export const BYTES_32_HEX_EXPR = '^0x[0-9a-fA-F]{64}$'; // '0x' + 32 bytes (64 chars)
99

1010
const POSITIVE_NUMBER_SCHEMA = {
1111
$id: "positive-number-schema",
@@ -73,10 +73,7 @@ const GLOBAL_SCHEMA = {
7373
$id: "global-schema",
7474
type: "object",
7575
properties: {
76-
privateKey: {
77-
type: "string",
78-
pattern: BYTES_32_HEX_EXPR,
79-
},
76+
privateKey: { $ref: "private-key-schema" },
8077
logLevel: { $ref: "non-empty-string-schema" },
8178

8279
monitor: { $ref: "monitor-schema" },
@@ -87,10 +84,28 @@ const GLOBAL_SCHEMA = {
8784
persister: { $ref: "persister-schema" },
8885
wallet: { $ref: "wallet-schema" },
8986
},
90-
required: ["privateKey"],
87+
required: [],
9188
additionalProperties: false
9289
}
9390

91+
const PRIVATE_KEY_SCHEMA = {
92+
$id: "private-key-schema",
93+
"anyOf": [
94+
{
95+
type: "string",
96+
pattern: BYTES_32_HEX_EXPR,
97+
},
98+
{
99+
type: "object",
100+
properties: {
101+
loader: { $ref: "non-empty-string-schema" },
102+
},
103+
required: ["loader"],
104+
additionalProperties: true,
105+
}
106+
]
107+
}
108+
94109
const MONITOR_SCHEMA = {
95110
$id: "monitor-schema",
96111
type: "object",
@@ -275,6 +290,7 @@ export function getConfigValidator(): AnyValidateFunction<unknown> {
275290
ajv.addSchema(PROCESSING_INTERVAL_SCHEMA);
276291
ajv.addSchema(CONFIG_SCHEMA);
277292
ajv.addSchema(GLOBAL_SCHEMA);
293+
ajv.addSchema(PRIVATE_KEY_SCHEMA);
278294
ajv.addSchema(MONITOR_SCHEMA);
279295
ajv.addSchema(GETTER_SCHEMA);
280296
ajv.addSchema(PRICING_SCHEMA);

src/config/config.service.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { readFileSync } from 'fs';
33
import * as yaml from 'js-yaml';
44
import dotenv from 'dotenv';
55
import { PRICING_SCHEMA, getConfigValidator } from './config.schema';
6-
import { GlobalConfig, ChainConfig, AMBConfig, GetterGlobalConfig, SubmitterGlobalConfig, PersisterConfig, WalletGlobalConfig, GetterConfig, SubmitterConfig, WalletConfig, MonitorConfig, MonitorGlobalConfig, PricingConfig, PricingGlobalConfig, EvaluatorGlobalConfig, EvaluatorConfig } from './config.types';
6+
import { GlobalConfig, ChainConfig, AMBConfig, GetterGlobalConfig, SubmitterGlobalConfig, PersisterConfig, WalletGlobalConfig, GetterConfig, SubmitterConfig, WalletConfig, MonitorConfig, MonitorGlobalConfig, PricingGlobalConfig, EvaluatorGlobalConfig, PricingConfig, EvaluatorConfig } from './config.types';
77
import { JsonRpcProvider } from 'ethers6';
8+
import { loadPrivateKeyLoader } from './privateKeyLoaders/privateKeyLoader';
89

910
@Injectable()
1011
export class ConfigService {
@@ -85,6 +86,21 @@ export class ConfigService {
8586
}
8687
}
8788

89+
private async loadPrivateKey(rawPrivateKeyConfig: any): Promise<string> {
90+
if (typeof rawPrivateKeyConfig === "string") {
91+
//NOTE: Using 'console.warn' as the logger is not available at this point. //TODO use logger
92+
console.warn('WARNING: the privateKey has been loaded from the configuration file. Consider storing the privateKey using an alternative safer method.')
93+
return rawPrivateKeyConfig;
94+
}
95+
96+
const privateKeyLoader = loadPrivateKeyLoader(
97+
rawPrivateKeyConfig?.['loader'] ?? null,
98+
rawPrivateKeyConfig ?? {},
99+
);
100+
101+
return privateKeyLoader.load();
102+
}
103+
88104
private loadGlobalConfig(): GlobalConfig {
89105
const rawGlobalConfig = this.rawConfig['global'];
90106

@@ -96,7 +112,7 @@ export class ConfigService {
96112

97113
return {
98114
port: parseInt(process.env['RELAYER_PORT']),
99-
privateKey: rawGlobalConfig.privateKey,
115+
privateKey: this.loadPrivateKey(rawGlobalConfig.privateKey),
100116
logLevel: rawGlobalConfig.logLevel,
101117
monitor: this.formatMonitorGlobalConfig(rawGlobalConfig.monitor),
102118
getter: this.formatGetterGlobalConfig(rawGlobalConfig.getter),

src/config/config.types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export interface GlobalConfig {
22
port: number;
3-
privateKey: string;
3+
privateKey: Promise<string>;
44
logLevel?: string;
55
monitor: MonitorGlobalConfig;
66
getter: GetterGlobalConfig;
@@ -11,6 +11,10 @@ export interface GlobalConfig {
1111
wallet: WalletGlobalConfig;
1212
}
1313

14+
export type PrivateKeyConfig = string | {
15+
loader: string;
16+
}
17+
1418
export interface MonitorGlobalConfig {
1519
interval?: number;
1620
blockDelay?: number;

src/config/privateKeyLoaders/env.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { PrivateKeyLoader, PrivateKeyLoaderConfig } from "./privateKeyLoader";
2+
3+
export const PRIVATE_KEY_LOADER_TYPE_ENVIRONMENT_VARIABLE = 'env';
4+
const DEFAULT_ENV_VARIABLE_NAME = 'RELAYER_PRIVATE_KEY';
5+
6+
export interface EnvPrivateKeyLoaderConfig extends PrivateKeyLoaderConfig {
7+
envVariableName?: string,
8+
}
9+
10+
export class EnvPrivateKeyLoader extends PrivateKeyLoader {
11+
override loaderType: string = PRIVATE_KEY_LOADER_TYPE_ENVIRONMENT_VARIABLE;
12+
private readonly envVariableName: string;
13+
14+
constructor(
15+
protected override readonly config: EnvPrivateKeyLoaderConfig,
16+
) {
17+
super(config);
18+
19+
this.envVariableName = config.envVariableName ?? DEFAULT_ENV_VARIABLE_NAME;
20+
}
21+
22+
override async loadPrivateKey(): Promise<string> {
23+
const privateKey = process.env[this.envVariableName];
24+
25+
if (privateKey == undefined) {
26+
throw new Error(
27+
`Failed to load privateKey from enviornment variable '${this.envVariableName}'.`,
28+
);
29+
}
30+
31+
return privateKey;
32+
}
33+
}
34+
35+
export default EnvPrivateKeyLoader;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { BYTES_32_HEX_EXPR } from "../config.schema";
2+
3+
export const PRIVATE_KEY_LOADER_TYPE_BASE = 'base';
4+
5+
const DEFAULT_PRIVATE_KEY_LOADER = 'env';
6+
7+
export interface PrivateKeyLoaderConfig {
8+
}
9+
10+
export function loadPrivateKeyLoader(
11+
loader: string | null,
12+
config: PrivateKeyLoaderConfig
13+
): BasePrivateKeyLoader {
14+
15+
// eslint-disable-next-line @typescript-eslint/no-var-requires
16+
const module = require(`./${loader ?? DEFAULT_PRIVATE_KEY_LOADER}`);
17+
const loaderClass: typeof BasePrivateKeyLoader = module.default;
18+
19+
return new loaderClass(
20+
config,
21+
)
22+
}
23+
24+
export abstract class PrivateKeyLoader {
25+
abstract readonly loaderType: string;
26+
27+
constructor(
28+
protected readonly config: PrivateKeyLoaderConfig,
29+
) {}
30+
31+
abstract loadPrivateKey(): Promise<string>;
32+
33+
async load(): Promise<string> {
34+
const privateKey = await this.loadPrivateKey();
35+
36+
if (!new RegExp(BYTES_32_HEX_EXPR).test(privateKey)) {
37+
throw new Error('Invalid loaded privateKey format.')
38+
}
39+
40+
return privateKey;
41+
}
42+
}
43+
44+
45+
// ! 'BasePrivateKeyLoader' should only be used as a type.
46+
export class BasePrivateKeyLoader extends PrivateKeyLoader {
47+
override loaderType: string = PRIVATE_KEY_LOADER_TYPE_BASE;
48+
override loadPrivateKey(): Promise<string> {
49+
throw new Error("Method not implemented.");
50+
}
51+
}

src/main.ts

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ async function bootstrap() {
2727
const configService = app.get(ConfigService);
2828
const loggerService = app.get(LoggerService);
2929

30+
// Wait for the privateKey to be ready
31+
await configService.globalConfig.privateKey;
32+
3033
logLoadedOptions(configService, loggerService);
3134

3235
await configService.isReady;

src/submitter/submitter.service.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class SubmitterService {
6363
async onModuleInit(): Promise<void> {
6464
this.loggerService.info(`Starting the submitter on all chains...`);
6565

66-
const globalSubmitterConfig = this.loadGlobalSubmitterConfig();
66+
const globalSubmitterConfig = await this.loadGlobalSubmitterConfig();
6767

6868
// check if the submitter has been disabled.
6969
if (!globalSubmitterConfig.enabled) {
@@ -109,7 +109,7 @@ export class SubmitterService {
109109
await new Promise((r) => setTimeout(r, 5000));
110110
}
111111

112-
private loadGlobalSubmitterConfig(): GlobalSubmitterConfig {
112+
private async loadGlobalSubmitterConfig(): Promise<GlobalSubmitterConfig> {
113113
const submitterConfig = this.configService.globalConfig.submitter;
114114

115115
const enabled = submitterConfig['enabled'] ?? true;
@@ -129,7 +129,7 @@ export class SubmitterService {
129129
const maxEvaluationDuration =
130130
submitterConfig.maxEvaluationDuration ?? MAX_EVALUATION_DURATION_DEFAULT;
131131

132-
const walletPublicKey = (new Wallet(this.configService.globalConfig.privateKey)).address;
132+
const walletPublicKey = (new Wallet(await this.configService.globalConfig.privateKey)).address;
133133

134134
return {
135135
enabled,

src/wallet/wallet.service.ts

+13-9
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,14 @@ export class WalletService implements OnModuleInit {
6767

6868
private readonly queuedMessages: Record<string, WalletServiceRoutingData[]> = {};
6969

70-
readonly publicKey: string;
70+
readonly publicKey: Promise<string>;
7171

7272
constructor(
7373
private readonly configService: ConfigService,
7474
private readonly loggerService: LoggerService,
7575
) {
7676
this.defaultWorkerConfig = this.loadDefaultWorkerConfig();
77-
this.publicKey = (new Wallet(this.configService.globalConfig.privateKey)).address;
77+
this.publicKey = this.loadPublicKey();
7878
}
7979

8080
async onModuleInit() {
@@ -85,10 +85,14 @@ export class WalletService implements OnModuleInit {
8585
this.initiateIntervalStatusLog();
8686
}
8787

88+
private async loadPublicKey(): Promise<string> {
89+
return (new Wallet(await this.configService.globalConfig.privateKey)).address;
90+
}
91+
8892
private async initializeWorkers(): Promise<void> {
8993

9094
for (const [chainId,] of this.configService.chainsConfig) {
91-
this.spawnWorker(chainId);
95+
await this.spawnWorker(chainId);
9296
}
9397

9498
// Add a small delay to wait for the workers to be initialized
@@ -134,9 +138,9 @@ export class WalletService implements OnModuleInit {
134138
}
135139
}
136140

137-
private loadWorkerConfig(
141+
private async loadWorkerConfig(
138142
chainId: string,
139-
): WalletWorkerData {
143+
): Promise<WalletWorkerData> {
140144

141145
const defaultConfig = this.defaultWorkerConfig;
142146

@@ -170,7 +174,7 @@ export class WalletService implements OnModuleInit {
170174
chainWalletConfig.gasBalanceUpdateInterval ??
171175
defaultConfig.balanceUpdateInterval,
172176

173-
privateKey: this.configService.globalConfig.privateKey,
177+
privateKey: await this.configService.globalConfig.privateKey,
174178

175179
maxFeePerGas:
176180
chainWalletConfig.maxFeePerGas ??
@@ -200,10 +204,10 @@ export class WalletService implements OnModuleInit {
200204
};
201205
}
202206

203-
private spawnWorker(
207+
private async spawnWorker(
204208
chainId: string
205-
): void {
206-
const workerData = this.loadWorkerConfig(chainId);
209+
): Promise<void> {
210+
const workerData = await this.loadWorkerConfig(chainId);
207211
this.loggerService.info(
208212
{
209213
chainId,

0 commit comments

Comments
 (0)