@lodestar/api
Version:
A Typescript REST client for the Ethereum Consensus API
700 lines (663 loc) • 21.9 kB
text/typescript
import {ContainerType, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {Epoch, phase0, ssz, stringType} from "@lodestar/types";
import {
EmptyArgs,
EmptyMeta,
EmptyMetaCodec,
EmptyRequest,
EmptyRequestCodec,
EmptyResponseCodec,
EmptyResponseData,
JsonOnlyReq,
JsonOnlyResponseCodec,
} from "../utils/codecs.js";
import {Endpoint, RouteDefinitions, Schema} from "../utils/index.js";
import {WireFormat} from "../utils/wireFormat.js";
export enum ImportStatus {
/** Keystore successfully decrypted and imported to keymanager permanent storage */
imported = "imported",
/** Keystore's pubkey is already known to the keymanager */
duplicate = "duplicate",
/** Any other status different to the above: decrypting error, I/O errors, etc. */
error = "error",
}
export enum DeletionStatus {
/** key was active and removed */
deleted = "deleted",
/** slashing protection data returned but key was not active */
not_active = "not_active",
/** key was not found to be removed, and no slashing data can be returned */
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 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 const FeeRecipientDataType = new ContainerType(
{
pubkey: stringType,
ethaddress: stringType,
},
{jsonCase: "eth2"}
);
export const GraffitiDataType = new ContainerType(
{
pubkey: stringType,
graffiti: stringType,
},
{jsonCase: "eth2"}
);
export const GasLimitDataType = new ContainerType(
{
pubkey: stringType,
gasLimit: ssz.UintNum64,
},
{jsonCase: "eth2"}
);
export const BuilderBoostFactorDataType = new ContainerType(
{
pubkey: stringType,
builderBoostFactor: ssz.UintBn64,
},
{jsonCase: "eth2"}
);
export type FeeRecipientData = ValueOf<typeof FeeRecipientDataType>;
export type GraffitiData = ValueOf<typeof GraffitiDataType>;
export type GasLimitData = ValueOf<typeof GasLimitDataType>;
export type BuilderBoostFactorData = ValueOf<typeof BuilderBoostFactorDataType>;
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;
};
export type RemoteSignerDefinition = Pick<SignerDefinition, "pubkey" | "url">;
export type ProposerConfigResponse = {
graffiti?: string;
strictFeeRecipientCheck?: boolean;
feeRecipient?: string;
builder?: {
gasLimit?: number;
selection?: string;
boostFactor?: string;
};
};
/**
* JSON serialized representation of a single keystore in EIP-2335: BLS12-381 Keystore format.
* ```
* '{"version":4,"uuid":"9f75a3fa-1e5a-49f9-be3d-f5a19779c6fa","path":"m/12381/3600/0/0/0","pubkey":"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a","crypto":{"kdf":{"function":"pbkdf2","params":{"dklen":32,"c":262144,"prf":"hmac-sha256","salt":"8ff8f22ef522a40f99c6ce07fdcfc1db489d54dfbc6ec35613edf5d836fa1407"},"message":""},"checksum":{"function":"sha256","params":{},"message":"9678a69833d2576e3461dd5fa80f6ac73935ae30d69d07659a709b3cd3eddbe3"},"cipher":{"function":"aes-128-ctr","params":{"iv":"31b69f0ac97261e44141b26aa0da693f"},"message":"e8228bafec4fcbaca3b827e586daad381d53339155b034e5eaae676b715ab05e"}}}'
* ```
*/
export type KeystoreStr = string;
/**
* JSON serialized representation of the slash protection data in format defined in EIP-3076: Slashing Protection Interchange Format.
* ```
* '{"metadata":{"interchange_format_version":"5","genesis_validators_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},"data":[{"pubkey":"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a","signed_blocks":[],"signed_attestations":[]}]}'
* ```
*/
export type SlashingProtectionData = string;
/**
* The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._
* ```
* "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
* ```
*/
export type PubkeyHex = string;
/**
* An address on the execution (Ethereum 1) network.
* ```
* "0xAbcF8e0d4e9587369b2301D0790347320302cc09"
* ```
*/
export type EthAddress = string;
/**
* Arbitrary data to set in the graffiti field of BeaconBlockBody
* ```
* "plain text value"
* ```
*/
export type Graffiti = string;
export type Endpoints = {
/**
* List all validating pubkeys known to and decrypted by this keymanager binary
*
* https://github.com/ethereum/keymanager-APIs/blob/0c975dae2ac6053c8245ebdb6a9f27c2f114f407/keymanager-oapi.yaml
*/
listKeys: Endpoint<
"GET",
EmptyArgs,
EmptyRequest,
{
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;
}[],
EmptyMeta
>;
/**
* Import keystores generated by the Eth2.0 deposit CLI tooling. `passwords[i]` must unlock `keystores[i]`.
*
* Users SHOULD send slashing_protection data associated with the imported pubkeys. MUST follow the format defined in
* EIP-3076: Slashing Protection Interchange Format.
*
* Returns status result of each `request.keystores` with same length and order of `request.keystores`
*
* https://github.com/ethereum/keymanager-APIs/blob/0c975dae2ac6053c8245ebdb6a9f27c2f114f407/keymanager-oapi.yaml
*/
importKeystores: Endpoint<
"POST",
{
/** JSON-encoded keystore files generated with the Launchpad */
keystores: KeystoreStr[];
/** Passwords to unlock imported keystore files. `passwords[i]` must unlock `keystores[i]` */
passwords: string[];
/** Slashing protection data for some of the keys of `keystores` */
slashingProtection?: SlashingProtectionData;
},
{body: {keystores: KeystoreStr[]; passwords: string[]; slashing_protection?: SlashingProtectionData}},
ResponseStatus<ImportStatus>[],
EmptyMeta
>;
/**
* DELETE must delete all keys from `request.pubkeys` that are known to the keymanager and exist in its
* persistent storage. Additionally, DELETE must fetch the slashing protection data for the requested keys from
* persistent storage, which must be retained (and not deleted) after the response has been sent. Therefore in the
* case of two identical delete requests being made, both will have access to slashing protection data.
*
* In a single atomic sequential operation the keymanager must:
* 1. Guarantee that key(s) can not produce any more signature; only then
* 2. Delete key(s) and serialize its associated slashing protection data
*
* DELETE should never return a 404 response, even if all pubkeys from request.pubkeys have no extant keystores
* nor slashing protection data.
*
* Slashing protection data must only be returned for keys from `request.pubkeys` for which a
* `deleted` or `not_active` status is returned.
*
* Returns deletion status of all keys in `request.pubkeys` in the same order.
*
* https://github.com/ethereum/keymanager-APIs/blob/0c975dae2ac6053c8245ebdb6a9f27c2f114f407/keymanager-oapi.yaml
*/
deleteKeys: Endpoint<
"DELETE",
{
/** List of public keys to delete */
pubkeys: PubkeyHex[];
},
{body: {pubkeys: string[]}},
{statuses: ResponseStatus<DeletionStatus>[]; slashingProtection: SlashingProtectionData},
EmptyMeta
>;
/**
* List all remote validating pubkeys known to this validator client binary
*/
listRemoteKeys: Endpoint<
// ⏎
"GET",
EmptyArgs,
EmptyRequest,
SignerDefinition[],
EmptyMeta
>;
/**
* Import remote keys for the validator client to request duties for
*/
importRemoteKeys: Endpoint<
"POST",
{remoteSigners: RemoteSignerDefinition[]},
{body: {remote_keys: RemoteSignerDefinition[]}},
ResponseStatus<ImportRemoteKeyStatus>[],
EmptyMeta
>;
/**
* DELETE must delete all keys from `request.pubkeys` that are known to the validator client and exist in its
* persistent storage.
*
* DELETE should never return a 404 response, even if all pubkeys from `request.pubkeys` have no existing keystores.
*/
deleteRemoteKeys: Endpoint<
"DELETE",
{pubkeys: PubkeyHex[]},
{body: {pubkeys: string[]}},
ResponseStatus<DeleteRemoteKeyStatus>[],
EmptyMeta
>;
listFeeRecipient: Endpoint<
// ⏎
"GET",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
FeeRecipientData,
EmptyMeta
>;
setFeeRecipient: Endpoint<
"POST",
{pubkey: PubkeyHex; ethaddress: EthAddress},
{params: {pubkey: string}; body: {ethaddress: string}},
EmptyResponseData,
EmptyMeta
>;
deleteFeeRecipient: Endpoint<
// ⏎
"DELETE",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
EmptyResponseData,
EmptyMeta
>;
getGraffiti: Endpoint<
// ⏎
"GET",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
GraffitiData,
EmptyMeta
>;
setGraffiti: Endpoint<
"POST",
{pubkey: PubkeyHex; graffiti: Graffiti},
{params: {pubkey: string}; body: {graffiti: string}},
EmptyResponseData,
EmptyMeta
>;
deleteGraffiti: Endpoint<
// ⏎
"DELETE",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
EmptyResponseData,
EmptyMeta
>;
getGasLimit: Endpoint<
// ⏎
"GET",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
GasLimitData,
EmptyMeta
>;
setGasLimit: Endpoint<
"POST",
{pubkey: PubkeyHex; gasLimit: number},
{params: {pubkey: string}; body: {gas_limit: string}},
EmptyResponseData,
EmptyMeta
>;
deleteGasLimit: Endpoint<
// ⏎
"DELETE",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
EmptyResponseData,
EmptyMeta
>;
getBuilderBoostFactor: Endpoint<
"GET",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
BuilderBoostFactorData,
EmptyMeta
>;
setBuilderBoostFactor: Endpoint<
"POST",
{pubkey: PubkeyHex; builderBoostFactor: bigint},
{params: {pubkey: string}; body: {builder_boost_factor: string}},
EmptyResponseData,
EmptyMeta
>;
deleteBuilderBoostFactor: Endpoint<
"DELETE",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
EmptyResponseData,
EmptyMeta
>;
getProposerConfig: Endpoint<
// ⏎
"GET",
{pubkey: PubkeyHex},
{params: {pubkey: string}},
ProposerConfigResponse,
EmptyMeta
>;
/**
* Create a signed voluntary exit message for an active validator, identified by a public key known to the validator
* client. This endpoint returns a `SignedVoluntaryExit` object, which can be used to initiate voluntary exit via the
* beacon node's [submitPoolVoluntaryExit](https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolVoluntaryExit) endpoint.
*
* Returns the signed voluntary exit message
*
* https://github.com/ethereum/keymanager-APIs/blob/7105e749e11dd78032ea275cc09bf62ecd548fca/keymanager-oapi.yaml
*/
signVoluntaryExit: Endpoint<
"POST",
{
/** Public key of an active validator known to the validator client */
pubkey: PubkeyHex;
/** Minimum epoch for processing exit. Defaults to the current epoch if not set */
epoch?: Epoch;
},
{params: {pubkey: string}; query: {epoch?: number}},
phase0.SignedVoluntaryExit,
EmptyMeta
>;
};
export function getDefinitions(_config: ChainForkConfig): RouteDefinitions<Endpoints> {
return {
listKeys: {
url: "/eth/v1/keystores",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
importKeystores: {
url: "/eth/v1/keystores",
method: "POST",
req: JsonOnlyReq({
writeReqJson: ({keystores, passwords, slashingProtection}) => ({
body: {keystores, passwords, slashing_protection: slashingProtection},
}),
parseReqJson: ({body: {keystores, passwords, slashing_protection}}) => ({
keystores,
passwords,
slashingProtection: slashing_protection,
}),
schema: {body: Schema.Object},
}),
resp: JsonOnlyResponseCodec,
},
deleteKeys: {
url: "/eth/v1/keystores",
method: "DELETE",
req: JsonOnlyReq({
writeReqJson: ({pubkeys}) => ({body: {pubkeys}}),
parseReqJson: ({body: {pubkeys}}) => ({pubkeys}),
schema: {body: Schema.Object},
}),
resp: {
onlySupport: WireFormat.json,
data: JsonOnlyResponseCodec.data,
meta: EmptyMetaCodec,
transform: {
toResponse: (data) => {
const {statuses, slashing_protection} = data as {
statuses: ResponseStatus<DeletionStatus>[];
slashing_protection: SlashingProtectionData;
};
return {data: statuses, slashing_protection};
},
fromResponse: (resp) => {
const {data, slashing_protection} = resp as {
data: ResponseStatus<DeletionStatus>[];
slashing_protection: SlashingProtectionData;
};
return {data: {statuses: data, slashingProtection: slashing_protection}};
},
},
},
},
listRemoteKeys: {
url: "/eth/v1/remotekeys",
method: "GET",
req: EmptyRequestCodec,
resp: JsonOnlyResponseCodec,
},
importRemoteKeys: {
url: "/eth/v1/remotekeys",
method: "POST",
req: JsonOnlyReq({
writeReqJson: ({remoteSigners}) => ({body: {remote_keys: remoteSigners}}),
parseReqJson: ({body: {remote_keys}}) => ({remoteSigners: remote_keys}),
schema: {body: Schema.Object},
}),
resp: JsonOnlyResponseCodec,
},
deleteRemoteKeys: {
url: "/eth/v1/remotekeys",
method: "DELETE",
req: JsonOnlyReq({
writeReqJson: ({pubkeys}) => ({body: {pubkeys}}),
parseReqJson: ({body: {pubkeys}}) => ({pubkeys}),
schema: {body: Schema.Object},
}),
resp: JsonOnlyResponseCodec,
},
listFeeRecipient: {
url: "/eth/v1/validator/{pubkey}/feerecipient",
method: "GET",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: {
onlySupport: WireFormat.json,
data: FeeRecipientDataType,
meta: EmptyMetaCodec,
},
},
setFeeRecipient: {
url: "/eth/v1/validator/{pubkey}/feerecipient",
method: "POST",
req: JsonOnlyReq({
writeReqJson: ({pubkey, ethaddress}) => ({params: {pubkey}, body: {ethaddress}}),
parseReqJson: ({params: {pubkey}, body: {ethaddress}}) => ({pubkey, ethaddress}),
schema: {
params: {pubkey: Schema.StringRequired},
body: Schema.Object,
},
}),
resp: EmptyResponseCodec,
},
deleteFeeRecipient: {
url: "/eth/v1/validator/{pubkey}/feerecipient",
method: "DELETE",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: EmptyResponseCodec,
},
getGraffiti: {
url: "/eth/v1/validator/{pubkey}/graffiti",
method: "GET",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: {
onlySupport: WireFormat.json,
data: GraffitiDataType,
meta: EmptyMetaCodec,
},
},
setGraffiti: {
url: "/eth/v1/validator/{pubkey}/graffiti",
method: "POST",
req: JsonOnlyReq({
writeReqJson: ({pubkey, graffiti}) => ({params: {pubkey}, body: {graffiti}}),
parseReqJson: ({params: {pubkey}, body: {graffiti}}) => ({pubkey, graffiti}),
schema: {
params: {pubkey: Schema.StringRequired},
body: Schema.Object,
},
}),
resp: EmptyResponseCodec,
},
deleteGraffiti: {
url: "/eth/v1/validator/{pubkey}/graffiti",
method: "DELETE",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: EmptyResponseCodec,
},
getGasLimit: {
url: "/eth/v1/validator/{pubkey}/gas_limit",
method: "GET",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: {
onlySupport: WireFormat.json,
data: GasLimitDataType,
meta: EmptyMetaCodec,
},
},
setGasLimit: {
url: "/eth/v1/validator/{pubkey}/gas_limit",
method: "POST",
req: JsonOnlyReq({
writeReqJson: ({pubkey, gasLimit}) => ({params: {pubkey}, body: {gas_limit: gasLimit.toString(10)}}),
parseReqJson: ({params: {pubkey}, body: {gas_limit}}) => ({pubkey, gasLimit: parseGasLimit(gas_limit)}),
schema: {
params: {pubkey: Schema.StringRequired},
body: Schema.Object,
},
}),
resp: EmptyResponseCodec,
},
deleteGasLimit: {
url: "/eth/v1/validator/{pubkey}/gas_limit",
method: "DELETE",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: EmptyResponseCodec,
},
getBuilderBoostFactor: {
url: "/eth/v1/validator/{pubkey}/builder_boost_factor",
method: "GET",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: {
onlySupport: WireFormat.json,
data: BuilderBoostFactorDataType,
meta: EmptyMetaCodec,
},
},
setBuilderBoostFactor: {
url: "/eth/v1/validator/{pubkey}/builder_boost_factor",
method: "POST",
req: JsonOnlyReq({
writeReqJson: ({pubkey, builderBoostFactor}) => ({
params: {pubkey},
body: {builder_boost_factor: builderBoostFactor.toString(10)},
}),
parseReqJson: ({params: {pubkey}, body: {builder_boost_factor}}) => ({
pubkey,
builderBoostFactor: BigInt(builder_boost_factor),
}),
schema: {
params: {pubkey: Schema.StringRequired},
body: Schema.Object,
},
}),
resp: EmptyResponseCodec,
},
deleteBuilderBoostFactor: {
url: "/eth/v1/validator/{pubkey}/builder_boost_factor",
method: "DELETE",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: EmptyResponseCodec,
},
getProposerConfig: {
url: "/eth/v0/validator/{pubkey}/proposer_config",
method: "GET",
req: {
writeReq: ({pubkey}) => ({params: {pubkey}}),
parseReq: ({params: {pubkey}}) => ({pubkey}),
schema: {
params: {pubkey: Schema.StringRequired},
},
},
resp: JsonOnlyResponseCodec,
},
signVoluntaryExit: {
url: "/eth/v1/validator/{pubkey}/voluntary_exit",
method: "POST",
req: {
writeReq: ({pubkey, epoch}) => ({params: {pubkey}, query: {epoch}}),
parseReq: ({params: {pubkey}, query: {epoch}}) => ({pubkey, epoch}),
schema: {
params: {pubkey: Schema.StringRequired},
query: {epoch: Schema.Uint},
},
},
resp: {
data: ssz.phase0.SignedVoluntaryExit,
meta: EmptyMetaCodec,
},
},
};
}
function parseGasLimit(gasLimitInput: string | number): number {
if ((typeof gasLimitInput !== "string" && typeof gasLimitInput !== "number") || `${gasLimitInput}`.trim() === "") {
throw Error("Not valid Gas Limit");
}
const gasLimit = Number(gasLimitInput);
if (Number.isNaN(gasLimit) || gasLimit === 0) {
throw Error(`Gas Limit is not valid gasLimit=${gasLimit}`);
}
return gasLimit;
}