Skip to content

Commit

Permalink
Implement remote key manager API (#4106)
Browse files Browse the repository at this point in the history
* Add remote keys server

* Add unit test in api

* Fix merge conflicts

* Update e2e tests

* Lint

* Re-add KEYSTORE_IMPORTED_PREFIX
  • Loading branch information
dapplion authored Jun 26, 2022
1 parent 0553523 commit f15a8a9
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 70 deletions.
13 changes: 12 additions & 1 deletion packages/api/src/keymanager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,18 @@ import * as keymanager from "./client.js";

// NOTE: Don't export server here so it's not bundled to all consumers

export {ImportStatus, DeletionStatus, KeystoreStr, SlashingProtectionData, PubkeyHex, Api} from "./routes.js";
export {
ImportStatus,
DeletionStatus,
ImportRemoteKeyStatus,
DeleteRemoteKeyStatus,
ResponseStatus,
SignerDefinition,
KeystoreStr,
SlashingProtectionData,
PubkeyHex,
Api,
} from "./routes.js";

type ClientModules = HttpClientModules & {
config: IChainForkConfig;
Expand Down
125 changes: 102 additions & 23 deletions packages/api/src/keymanager/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,44 @@ export enum DeletionStatus {
error = "error",
}

export enum ImportRemoteKeyStatus {
/** Remote key successfully imported to validator client permanent storage */
imported = "imported",
/** Remote key's pubkey is already known to the validator client */
duplicate = "duplicate",
/** Any other status different to the above: I/O errors, etc. */
error = "error",
}

export enum DeleteRemoteKeyStatus {
/** key was active and removed */
deleted = "deleted",
/** key was not found to be removed */
not_found = "not_found",
/**
* unexpected condition meant the key could not be removed (the key was actually found,
* but we couldn't stop using it) - this would be a sign that making it active elsewhere would
* almost certainly cause you headaches / slashing conditions etc.
*/
error = "error",
}

export type ResponseStatus<Status> = {
status: Status;
message?: string;
};

export type SignerDefinition = {
pubkey: PubkeyHex;
/**
* URL to API implementing EIP-3030: BLS Remote Signer HTTP API
* `"https://remote.signer"`
*/
url: string;
/** The signer associated with this pubkey cannot be deleted from the API */
readonly: boolean;
};

/**
* JSON serialized representation of a single keystore in EIP-2335: BLS12-381 Keystore format.
* ```
Expand All @@ -44,33 +82,20 @@ export type SlashingProtectionData = string;
*/
export type PubkeyHex = string;

type Statuses<Status> = {
status: Status;
message?: string;
}[];

type ImportKeystoresReq = {
keystores: KeystoreStr[];
passwords: string[];
slashingProtection: SlashingProtectionData;
};

type ListKeysResponse = {
validatingPubkey: PubkeyHex;
/** The derivation path (if present in the imported keystore) */
derivationPath?: string;
/** The key associated with this pubkey cannot be deleted from the API */
readonly?: boolean;
};

export type Api = {
/**
* List all validating pubkeys known to and decrypted by this keymanager binary
*
* https://github.com/ethereum/keymanager-APIs/blob/0c975dae2ac6053c8245ebdb6a9f27c2f114f407/keymanager-oapi.yaml
*/
listKeys(): Promise<{
data: ListKeysResponse[];
data: {
validatingPubkey: PubkeyHex;
/** The derivation path (if present in the imported keystore) */
derivationPath?: string;
/** The key associated with this pubkey cannot be deleted from the API */
readonly?: boolean;
}[];
}>;

/**
Expand All @@ -91,7 +116,7 @@ export type Api = {
passwords: string[],
slashingProtectionStr: SlashingProtectionData
): Promise<{
data: Statuses<ImportStatus>;
data: ResponseStatus<ImportStatus>[];
}>;

/**
Expand All @@ -118,21 +143,59 @@ export type Api = {
deleteKeystores(
pubkeysHex: string[]
): Promise<{
data: Statuses<DeletionStatus>;
data: ResponseStatus<DeletionStatus>[];
slashingProtection: SlashingProtectionData;
}>;

/**
* List all remote validating pubkeys known to this validator client binary
*/
listRemoteKeys(): Promise<{data: SignerDefinition[]}>;

/**
* Import remote keys for the validator client to request duties for
*/
importRemoteKeys(
remoteSigners: Pick<SignerDefinition, "pubkey" | "url">[]
): Promise<{
data: ResponseStatus<ImportRemoteKeyStatus>[];
}>;

deleteRemoteKeys(
pubkeys: PubkeyHex[]
): Promise<{
data: ResponseStatus<DeleteRemoteKeyStatus>[];
}>;
};

export const routesData: RoutesData<Api> = {
listKeys: {url: "/eth/v1/keystores", method: "GET"},
importKeystores: {url: "/eth/v1/keystores", method: "POST"},
deleteKeystores: {url: "/eth/v1/keystores", method: "DELETE"},

listRemoteKeys: {url: "/eth/v1/remotekeys", method: "GET"},
importRemoteKeys: {url: "/eth/v1/remotekeys", method: "POST"},
deleteRemoteKeys: {url: "/eth/v1/remotekeys", method: "DELETE"},
};

export type ReqTypes = {
listKeys: ReqEmpty;
importKeystores: {body: ImportKeystoresReq};
importKeystores: {
body: {
keystores: KeystoreStr[];
passwords: string[];
slashingProtection: SlashingProtectionData;
};
};
deleteKeystores: {body: {pubkeys: string[]}};

listRemoteKeys: ReqEmpty;
importRemoteKeys: {
body: {
remoteKeys: Pick<SignerDefinition, "pubkey" | "url">[];
};
};
deleteRemoteKeys: {body: {pubkeys: string[]}};
};

export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
Expand All @@ -148,6 +211,18 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
parseReq: ({body: {pubkeys}}) => [pubkeys],
schema: {body: Schema.Object},
},

listRemoteKeys: reqEmpty,
importRemoteKeys: {
writeReq: (remoteKeys) => ({body: {remoteKeys}}),
parseReq: ({body: {remoteKeys}}) => [remoteKeys],
schema: {body: Schema.Object},
},
deleteRemoteKeys: {
writeReq: (pubkeys) => ({body: {pubkeys}}),
parseReq: ({body: {pubkeys}}) => [pubkeys],
schema: {body: Schema.Object},
},
};
}

Expand All @@ -157,5 +232,9 @@ export function getReturnTypes(): ReturnTypes<Api> {
listKeys: jsonType("camel"),
importKeystores: jsonType("camel"),
deleteKeystores: jsonType("camel"),

listRemoteKeys: jsonType("camel"),
importRemoteKeys: jsonType("camel"),
deleteRemoteKeys: jsonType("camel"),
};
}
38 changes: 34 additions & 4 deletions packages/api/test/unit/keymanager/keymanager.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import {config} from "@chainsafe/lodestar-config/default";
import {Api, DeletionStatus, ImportStatus, ReqTypes} from "../../../src/keymanager/routes.js";
import {
Api,
DeleteRemoteKeyStatus,
DeletionStatus,
ImportRemoteKeyStatus,
ImportStatus,
ReqTypes,
} from "../../../src/keymanager/routes.js";
import {getClient} from "../../../src/keymanager/client.js";
import {getRoutes} from "../../../src/keymanager/server/index.js";
import {runGenericServerTest} from "../../utils/genericServerTest.js";

describe("keymanager", () => {
// randomly pregenerated pubkey
const pubkeyRand =
"0x84105a985058fc8740a48bf1ede9d223ef09e8c6b1735ba0a55cf4a9ff2ff92376b778798365e488dab07a652eb04576";

runGenericServerTest<Api, ReqTypes>(config, getClient, getRoutes, {
listKeys: {
args: [],
res: {
data: [
{
validatingPubkey:
// randomly pregenerated pubkey
"0x84105a985058fc8740a48bf1ede9d223ef09e8c6b1735ba0a55cf4a9ff2ff92376b778798365e488dab07a652eb04576",
validatingPubkey: pubkeyRand,
derivationPath: "m/12381/3600/0/0/0",
readonly: false,
},
Expand All @@ -28,5 +37,26 @@ describe("keymanager", () => {
args: [["key1"]],
res: {data: [{status: DeletionStatus.deleted}], slashingProtection: "slash_protection"},
},

listRemoteKeys: {
args: [],
res: {
data: [
{
pubkey: pubkeyRand,
url: "https://sign.er",
readonly: false,
},
],
},
},
importRemoteKeys: {
args: [[{pubkey: pubkeyRand, url: "https://sign.er"}]],
res: {data: [{status: ImportRemoteKeyStatus.imported}]},
},
deleteRemoteKeys: {
args: [["key1"]],
res: {data: [{status: DeleteRemoteKeyStatus.deleted}]},
},
});
});
11 changes: 8 additions & 3 deletions packages/cli/src/cmds/validator/handler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {LevelDbController} from "@chainsafe/lodestar-db";
import {SignerType, Signer, SlashingProtection, Validator} from "@chainsafe/lodestar-validator";
import {SlashingProtection, Validator} from "@chainsafe/lodestar-validator";
import {SignerType, Signer} from "@chainsafe/lodestar-validator";
import {getMetrics, MetricsRegister} from "@chainsafe/lodestar-validator";
import {KeymanagerServer, KeymanagerApi} from "@chainsafe/lodestar-keymanager-server";
import {RegistryMetricCreator, collectNodeJSMetrics, HttpMetricsServer} from "@chainsafe/lodestar";
Expand All @@ -9,7 +10,7 @@ import {YargsError, getDefaultGraffiti, mkdir, getCliLogger} from "../../util/in
import {onGracefulShutdown, parseFeeRecipient} from "../../util/index.js";
import {getVersionData} from "../../util/version.js";
import {getBeaconPaths} from "../beacon/paths.js";
import {getValidatorPaths} from "./paths.js";
import {getAccountPaths, getValidatorPaths} from "./paths.js";
import {IValidatorCliArgs, validatorMetricsDefaultOptions, defaultDefaultFeeRecipient} from "./options.js";
import {getLocalSecretKeys, getExternalSigners, groupExternalSignersByUrl} from "./keys.js";

Expand Down Expand Up @@ -145,8 +146,12 @@ export async function validatorHandler(args: IValidatorCliArgs & IGlobalArgs): P
// Use the first path in importKeystoresPath as directory to write keystores
// KeymanagerApi must ensure that the path is a directory and not a file
const firstImportKeystorePath = args.importKeystoresPath[0];
const accountPaths = getAccountPaths(args);

const keymanagerApi = new KeymanagerApi(validator, firstImportKeystorePath);
const keymanagerApi = new KeymanagerApi(validator, {
importedKeystoresDirpath: firstImportKeystorePath,
importedRemoteKeysDirpath: accountPaths.remoteKeysDir,
});

const keymanagerServer = new KeymanagerServer(
{
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/cmds/validator/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type AccountPaths = {
keystoresDir: string;
secretsDir: string;
walletsDir: string;
remoteKeysDir: string;
};

/**
Expand Down Expand Up @@ -74,11 +75,13 @@ export function getAccountPaths(
const keystoresDir = args.keystoresDir || path.join(rootDir, "keystores");
const secretsDir = args.secretsDir || path.join(rootDir, "secrets");
const walletsDir = args.walletsDir || path.join(rootDir, "wallets");
const remoteKeysDir = args.walletsDir || path.join(rootDir, "remoteKeys");
return {
...globalPaths,
keystoresDir,
secretsDir,
walletsDir,
remoteKeysDir,
};
}

Expand Down
Loading

0 comments on commit f15a8a9

Please sign in to comment.