Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat]: Overhaul private key management #27

Merged
merged 5 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Global Relayer configuration
global:
privateKey: '' # The privateKey of the account that will be submitting the packet relays
# ! The 'privateKey' of the account that will be submitting the packet relays is by default
# ! loaded from the environment variable 'RELAYER_PRIVATE_KEY'. Alternatively, the privateKey
# ! may be specified here (not recommended).
# privateKey: ''
# ! Optionally, custom privateKey loaders may be implemented and specified (NOTE: the 'env'
# ! loader is used if no privateKey configuration is specified):
# privateKey:
# loader: 'env' # The privateKey loader name (must match the implementation on src/config/privateKeyLoaders/<loader>.ts).
# customLoaderConfig: '' # Custom loader configs may be specified.

logLevel: 'info'

monitor:
Expand Down
30 changes: 23 additions & 7 deletions src/config/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { AnyValidateFunction } from "ajv/dist/core"
const MIN_PROCESSING_INTERVAL = 1;
const MAX_PROCESSING_INTERVAL = 500;

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

const POSITIVE_NUMBER_SCHEMA = {
$id: "positive-number-schema",
Expand Down Expand Up @@ -73,10 +73,7 @@ const GLOBAL_SCHEMA = {
$id: "global-schema",
type: "object",
properties: {
privateKey: {
type: "string",
pattern: BYTES_32_HEX_EXPR,
},
privateKey: { $ref: "private-key-schema" },
logLevel: { $ref: "non-empty-string-schema" },

monitor: { $ref: "monitor-schema" },
Expand All @@ -87,10 +84,28 @@ const GLOBAL_SCHEMA = {
persister: { $ref: "persister-schema" },
wallet: { $ref: "wallet-schema" },
},
required: ["privateKey"],
required: [],
additionalProperties: false
}

const PRIVATE_KEY_SCHEMA = {
$id: "private-key-schema",
"anyOf": [
{
type: "string",
pattern: BYTES_32_HEX_EXPR,
},
{
type: "object",
properties: {
loader: { $ref: "non-empty-string-schema" },
},
required: ["loader"],
additionalProperties: true,
}
]
}

const MONITOR_SCHEMA = {
$id: "monitor-schema",
type: "object",
Expand Down Expand Up @@ -275,6 +290,7 @@ export function getConfigValidator(): AnyValidateFunction<unknown> {
ajv.addSchema(PROCESSING_INTERVAL_SCHEMA);
ajv.addSchema(CONFIG_SCHEMA);
ajv.addSchema(GLOBAL_SCHEMA);
ajv.addSchema(PRIVATE_KEY_SCHEMA);
ajv.addSchema(MONITOR_SCHEMA);
ajv.addSchema(GETTER_SCHEMA);
ajv.addSchema(PRICING_SCHEMA);
Expand Down
20 changes: 18 additions & 2 deletions src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { readFileSync } from 'fs';
import * as yaml from 'js-yaml';
import dotenv from 'dotenv';
import { PRICING_SCHEMA, getConfigValidator } from './config.schema';
import { GlobalConfig, ChainConfig, AMBConfig, GetterGlobalConfig, SubmitterGlobalConfig, PersisterConfig, WalletGlobalConfig, GetterConfig, SubmitterConfig, WalletConfig, MonitorConfig, MonitorGlobalConfig, PricingConfig, PricingGlobalConfig, EvaluatorGlobalConfig, EvaluatorConfig } from './config.types';
import { GlobalConfig, ChainConfig, AMBConfig, GetterGlobalConfig, SubmitterGlobalConfig, PersisterConfig, WalletGlobalConfig, GetterConfig, SubmitterConfig, WalletConfig, MonitorConfig, MonitorGlobalConfig, PricingGlobalConfig, EvaluatorGlobalConfig, PricingConfig, EvaluatorConfig } from './config.types';
import { JsonRpcProvider } from 'ethers6';
import { loadPrivateKeyLoader } from './privateKeyLoaders/privateKeyLoader';

@Injectable()
export class ConfigService {
Expand Down Expand Up @@ -85,6 +86,21 @@ export class ConfigService {
}
}

private async loadPrivateKey(rawPrivateKeyConfig: any): Promise<string> {
if (typeof rawPrivateKeyConfig === "string") {
//NOTE: Using 'console.warn' as the logger is not available at this point. //TODO use logger
console.warn('WARNING: the privateKey has been loaded from the configuration file. Consider storing the privateKey using an alternative safer method.')
return rawPrivateKeyConfig;
}

const privateKeyLoader = loadPrivateKeyLoader(
rawPrivateKeyConfig?.['loader'] ?? null,
rawPrivateKeyConfig ?? {},
);

return privateKeyLoader.load();
}

private loadGlobalConfig(): GlobalConfig {
const rawGlobalConfig = this.rawConfig['global'];

Expand All @@ -96,7 +112,7 @@ export class ConfigService {

return {
port: parseInt(process.env['RELAYER_PORT']),
privateKey: rawGlobalConfig.privateKey,
privateKey: this.loadPrivateKey(rawGlobalConfig.privateKey),
logLevel: rawGlobalConfig.logLevel,
monitor: this.formatMonitorGlobalConfig(rawGlobalConfig.monitor),
getter: this.formatGetterGlobalConfig(rawGlobalConfig.getter),
Expand Down
6 changes: 5 additions & 1 deletion src/config/config.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface GlobalConfig {
port: number;
privateKey: string;
privateKey: Promise<string>;
logLevel?: string;
monitor: MonitorGlobalConfig;
getter: GetterGlobalConfig;
Expand All @@ -11,6 +11,10 @@ export interface GlobalConfig {
wallet: WalletGlobalConfig;
}

export type PrivateKeyConfig = string | {
loader: string;
}

export interface MonitorGlobalConfig {
interval?: number;
blockDelay?: number;
Expand Down
35 changes: 35 additions & 0 deletions src/config/privateKeyLoaders/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { PrivateKeyLoader, PrivateKeyLoaderConfig } from "./privateKeyLoader";

export const PRIVATE_KEY_LOADER_TYPE_ENVIRONMENT_VARIABLE = 'env';
const DEFAULT_ENV_VARIABLE_NAME = 'RELAYER_PRIVATE_KEY';

export interface EnvPrivateKeyLoaderConfig extends PrivateKeyLoaderConfig {
envVariableName?: string,
}

export class EnvPrivateKeyLoader extends PrivateKeyLoader {
override loaderType: string = PRIVATE_KEY_LOADER_TYPE_ENVIRONMENT_VARIABLE;
private readonly envVariableName: string;

constructor(
protected override readonly config: EnvPrivateKeyLoaderConfig,
) {
super(config);

this.envVariableName = config.envVariableName ?? DEFAULT_ENV_VARIABLE_NAME;
}

override async loadPrivateKey(): Promise<string> {
const privateKey = process.env[this.envVariableName];

if (privateKey == undefined) {
throw new Error(
`Failed to load privateKey from enviornment variable '${this.envVariableName}'.`,
);
}

return privateKey;
}
}

export default EnvPrivateKeyLoader;
51 changes: 51 additions & 0 deletions src/config/privateKeyLoaders/privateKeyLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BYTES_32_HEX_EXPR } from "../config.schema";

export const PRIVATE_KEY_LOADER_TYPE_BASE = 'base';

const DEFAULT_PRIVATE_KEY_LOADER = 'env';

export interface PrivateKeyLoaderConfig {
}

export function loadPrivateKeyLoader(
loader: string | null,
config: PrivateKeyLoaderConfig
): BasePrivateKeyLoader {

// eslint-disable-next-line @typescript-eslint/no-var-requires
const module = require(`./${loader ?? DEFAULT_PRIVATE_KEY_LOADER}`);
const loaderClass: typeof BasePrivateKeyLoader = module.default;

return new loaderClass(
config,
)
}

export abstract class PrivateKeyLoader {
abstract readonly loaderType: string;

constructor(
protected readonly config: PrivateKeyLoaderConfig,
) {}

abstract loadPrivateKey(): Promise<string>;

async load(): Promise<string> {
const privateKey = await this.loadPrivateKey();

if (!new RegExp(BYTES_32_HEX_EXPR).test(privateKey)) {
throw new Error('Invalid loaded privateKey format.')
}

return privateKey;
}
}


// ! 'BasePrivateKeyLoader' should only be used as a type.
export class BasePrivateKeyLoader extends PrivateKeyLoader {
override loaderType: string = PRIVATE_KEY_LOADER_TYPE_BASE;
override loadPrivateKey(): Promise<string> {
throw new Error("Method not implemented.");
}
}
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ async function bootstrap() {
const configService = app.get(ConfigService);
const loggerService = app.get(LoggerService);

// Wait for the privateKey to be ready
await configService.globalConfig.privateKey;

logLoadedOptions(configService, loggerService);

await configService.isReady;
Expand Down
6 changes: 3 additions & 3 deletions src/submitter/submitter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class SubmitterService {
async onModuleInit(): Promise<void> {
this.loggerService.info(`Starting the submitter on all chains...`);

const globalSubmitterConfig = this.loadGlobalSubmitterConfig();
const globalSubmitterConfig = await this.loadGlobalSubmitterConfig();

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

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

const enabled = submitterConfig['enabled'] ?? true;
Expand All @@ -129,7 +129,7 @@ export class SubmitterService {
const maxEvaluationDuration =
submitterConfig.maxEvaluationDuration ?? MAX_EVALUATION_DURATION_DEFAULT;

const walletPublicKey = (new Wallet(this.configService.globalConfig.privateKey)).address;
const walletPublicKey = (new Wallet(await this.configService.globalConfig.privateKey)).address;

return {
enabled,
Expand Down
22 changes: 13 additions & 9 deletions src/wallet/wallet.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ export class WalletService implements OnModuleInit {

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

readonly publicKey: string;
readonly publicKey: Promise<string>;

constructor(
private readonly configService: ConfigService,
private readonly loggerService: LoggerService,
) {
this.defaultWorkerConfig = this.loadDefaultWorkerConfig();
this.publicKey = (new Wallet(this.configService.globalConfig.privateKey)).address;
this.publicKey = this.loadPublicKey();
}

async onModuleInit() {
Expand All @@ -85,10 +85,14 @@ export class WalletService implements OnModuleInit {
this.initiateIntervalStatusLog();
}

private async loadPublicKey(): Promise<string> {
return (new Wallet(await this.configService.globalConfig.privateKey)).address;
}

private async initializeWorkers(): Promise<void> {

for (const [chainId,] of this.configService.chainsConfig) {
this.spawnWorker(chainId);
await this.spawnWorker(chainId);
}

// Add a small delay to wait for the workers to be initialized
Expand Down Expand Up @@ -134,9 +138,9 @@ export class WalletService implements OnModuleInit {
}
}

private loadWorkerConfig(
private async loadWorkerConfig(
chainId: string,
): WalletWorkerData {
): Promise<WalletWorkerData> {

const defaultConfig = this.defaultWorkerConfig;

Expand Down Expand Up @@ -170,7 +174,7 @@ export class WalletService implements OnModuleInit {
chainWalletConfig.gasBalanceUpdateInterval ??
defaultConfig.balanceUpdateInterval,

privateKey: this.configService.globalConfig.privateKey,
privateKey: await this.configService.globalConfig.privateKey,

maxFeePerGas:
chainWalletConfig.maxFeePerGas ??
Expand Down Expand Up @@ -200,10 +204,10 @@ export class WalletService implements OnModuleInit {
};
}

private spawnWorker(
private async spawnWorker(
chainId: string
): void {
const workerData = this.loadWorkerConfig(chainId);
): Promise<void> {
const workerData = await this.loadWorkerConfig(chainId);
this.loggerService.info(
{
chainId,
Expand Down